Before any discovery is presented to the public, it undergoes various tests. Of course, the systems and applications developed by programmers are no exception to this. Today, we will focus more on testing our applications.
Unit Test
The word Unit can be translated from English as "point" or "part." When you want to test a specific part of the program, you write unit tests for it. Imagine you are a professor at a university, and you are testing the code written by 10,000 students. As a human, it’s impossible to check them quickly, but computers can do this. For a simpler example:
# Input: join_words("hello", "it's", "me", "Otabek")
# Output: "hello it's me Otabek"
def join_words(*words: tuple[str], sep: str = " ") -> str:
return sep.join(words)
Let’s consider the function above: you input several words, and the function combines them (you can separate the words in any desired way using sep
). All students submit their homework, and a simple unit test is enough to check them.
import unittest # Import unittest
class TestJoinWords(unittest.TestCase): # Start class name with Test...
# It’s best to name methods starting with test_...
def test_simple(self):
# assertEqual checks for equality, so -> add(1, 2) == 3
# This means when you run the function("value"), its result should be "result"
self.assertEqual(join_words("it's", "me"), "it's me")
self.assertEqual(join_words("testing", "in", "action", sep="-"), "testing-in-action")
if __name__ == '__main__':
unittest.main()
I recommend researching unit tests in more detail and writing more tests afterwards. Unit tests are best written for classes, methods, and functions. I believe they are the best choice to ensure everything is functioning correctly.
Integration Test
Let’s say you have a program that serves thousands of users well. You’ve added new features to it. To ensure that the new features integrate successfully with the program, work flawlessly with other services, and check for any other potential issues, you would write Integration tests.
The word Integration means joining. So, when you integrate a new feature with an existing system, you write tests to ensure nothing breaks.
I know of two types of integration tests: Big Bang and Incremental. As the name suggests, the Big Bang test involves testing all parts of the program at once, and this is typically used in small projects because it naturally takes a lot of time and resources in large projects. In the Incremental type, you test each module or feature in smaller groups. This is more commonly seen in large applications, and companies that use monorepo approaches typically use this type of testing.
While unit tests test a small function or class methods, integration tests check if they work together properly and return the expected result.
# For example, let's test a simple API server
def user_info(name: str, age: int) -> dict:
return {"user": name, "age": age}
def display_user_info(data: dict) -> str:
return f"User {data['user']} is {data['age']} years old."
def get_user_info(name: str, age: int) -> str:
data = get_data(name, age)
return display_user(data)
# Integration test part
assert get_user_info("Otabek", 23) == "User Otabek is 23 years old."
assert get_user_info("John", 14) == "User John is 14 years old."
Functional Test
The goal of functional tests is to answer the question "Is this thing working as we expect it to?". These tests are typically used to check if business logic is working correctly. Business logic refers to how a business solves its problems as described. The main focus is not on how the program works but on whether it can solve the problem correctly. I believe no further example is needed for this concept. : )
E2E (End-to-End) Test
The concept of end-to-end can be translated as from "start to finish," but it’s a broader concept. Here, the word end is used twice:
- The first end refers to the server. It is primarily responsible for servicing requests and handling incoming queries.
- The second end refers to the user. This is the side that interacts with the server and consumes its services.
For the user, the last destination to receive the message is considered the end. For the server, if the message reaches the user, the task is completed. Performing end-to-end testing means that the tests you write should behave just like a real user would. This means that before your servers provide services to real users, the system should be tested to behave just like they would interact with it. It’s hard to explain with code, but I hope you understand (if code were written, this could be a Git repository, not an article).
Canary Test
In the past, miners would release a bird into a mine before entering to check if the air was safe. If the bird didn’t die, it was safe to enter. If it did, no one would go inside. The same concept applies in programming. We often deploy a smaller version of a system for a small group of users to check for bugs. This process is known as canary deployment, and the people you trust try the new features. If there are issues, they report them to the tech team. If everything works, the system is released to the public.
Stress Test
Stress testing involves pushing the system to its limits, by making heavy requests or operations to see if it can handle the load without crashing. For instance, if a user sends endless requests through a loop, stress testing ensures the server doesn’t shut down. Similarly, if a user works on multiple large files at once, stress testing ensures the program performs smoothly without freezing. Here's a simple stress test example:
def test_infinite_loop():
try:
while True:
append_new_users()
except MemoryError:
print("Oops! We hit a breaking point!")
def test_heavy_tasks():
try:
while True:
do_heavy_task()
except MemoryError:
print("Oops! We hit a breaking point!")
UI Testing
UI tests are written to check the User Interface of your program. You may want to check if your application is pixel perfect or whether specific texts appear on the page, or if classes, colors, and attributes are correct. For example, to check if a simple text exists in React, we would write a test like this:
// Writing a simple UI test with React
import { render, screen } from '@testing-library/react'
import App from './App'
test('renders welcome message', () => {
render(<App />)
expect(screen.getByText(/welcome/i)).toBeInTheDocument()
})
Monkey Test
Have you ever given a device to a monkey? If you haven’t, you might want to try it (just kidding). When a monkey gets a new device, it randomly presses buttons and does things you can’t predict. This test, invented by Netflix, is highly effective. If the program passes the monkey test, you shouldn’t worry about its performance with real users.
import random
def click_button(buttons):
return random.choice(buttons)
# Randomly pressing buttons for the test
buttons = ['Save', 'Cancel', 'Delete']
clicked_button = click_button(buttons)
assert clicked_button in buttons # We hope this doesn’t break
The above test is just a basic overview. In practice, monkey tests are much more complex.
Fuzz Testing
For example, you are given fields for first_name and last_name, and everyone knows they should write names there. However, some may enter numbers, true/false, {"name": "Jprqjon"}, or even 👑KING👑. If your function is designed to accept only numbers, the important thing is that the system doesn't crash and provides a proper warning to the user. In such cases, you need to write fuzz (uncertain) tests. Here’s an example of a simpler test written in Go (because of static typing):
package main
import (
"fmt"
"testing"
)
// Function that reverses strings
func Reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
// Fuzz test function
func TestReverse(f *testing.F) {
testCases := []string{"hello", "world", "", "a", "racecar", "Go is fun!"}
for _, tc := range testCases {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, input string) {
reversed := Reverse(input)
doubleReversed := Reverse(reversed)
if input != doubleReversed {
t.Errorf("Expected %q but got %q", input, doubleReversed)
}
if len(reversed) != len(input) {
t.Errorf("LengthError: input length %d, reversed length %d", len(input), len(reversed))
}
})
}
func main() {
fmt.Println("Run `go test` to execute fuzz testing.")
}
Others
There are many other types of tests available. In this article, I tried to cover as many as possible with examples. The rest can be found on the internet or discovered through hands-on experience.
We may discuss regression tests, security tests, A-B tests, smoke tests, load tests, performance tests, acceptance tests, and more in the future.