Tuple Logo

SHARE

What is Unit Testing? Fewer bugs, better code

sefa-senturk
Sefa Şentürk
2025-03-10 11:35 - 15 minutes
Software Development

Unit testing is a fundamental technique in software development in which individual components of an application, also known as “units,” are tested to make sure they function correctly. By using unit testing, developers can detect errors early and prevent bugs from causing major problems later in the development process.

What is unit testing?

Unit testing is a software testing method in which individual pieces of code, such as functions, methods or classes, are tested in isolation to verify that they work as expected. This is typically done with automated testing frameworks, such as JUnit for Java, PyTest for Python and NUnit for .NET.

Why is unit testing important?

Unit testing offers several benefits that contribute to software quality and stability:

How does unit testing work?

Unit testing works by isolating individual components of an application and testing them for correctness. This is usually done using test frameworks that run automated tests.

The basics of unit testing

Unit tests follow a simple pattern:

This is known as the AAA (Arrange, Act, Assert) principle and helps structure clear and understandable tests.

How are unit tests performed?

Unit tests can be performed in several ways:

What do unit tests look like?

Here is a simple example of a unit test in Python using the framework unittest:

import unittest

def add(a, b):
    return a + b

class TestMathOperations(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)
        self.assertEqual(add(-1, 1), 0)
        self.assertEqual(add(0, 0), 0)

if __name__ == '__main__':
    unittest.main()

Test explanation:

Unit testing strategies

To deploy unit tests effectively, developers use different strategies. A good unit test, not only covers the normal operation of a function, but also tests edge cases and error handling.

Logical checks

Logical checks test whether a function produces the expected outcomes based on the given input. This helps detect programming errors and incorrect logic.

Example:

def is_even(number):
    return number % 2 == 0

import unittest

class TestEvenFunction(unittest.TestCase):
    def test_is_even(self):
        self.assertTrue(is_even(4))
        self.assertFalse(is_even(3))

if __name__ == '__main__':
    unittest.main()

Here we test whether is_even(4) returns True and is_even(3) returns False.

Limit tests

Limit tests check how a function handles minimum, maximum and out-of-bounds input values. This prevents problems with extreme or unforeseen inputs.

Example:

A function that divides a number by another number must handle zero values correctly.

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

class TestDivideFunction(unittest.TestCase):
    def test_divide(self):
        self.assertEqual(divide(10, 2), 5)
        self.assertEqual(divide(-10, 2), -5)
    
    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            divide(10, 0)

if __name__ == '__main__':
    unittest.main()

This test checks that the function handles negative numbers correctly and throws an error when dividing by zero.

Error handling

Testing exception handling is important to verify that the code handles errors properly.

Example:

def safe_list_access(lst, index):
    try:
        return lst[index]
    except IndexError:
        return "Index out of range"

class TestSafeListAccess(unittest.TestCase):
    def test_valid_index(self):
        self.assertEqual(safe_list_access([1, 2, 3], 1), 2)
    
    def test_invalid_index(self):
        self.assertEqual(safe_list_access([1, 2, 3], 5), "Index out of range")

if __name__ == '__main__':
    unittest.main()

This test checks that an out-of-range list index is handled cleanly without crashing the application.

Object-oriented checks

In object-oriented programming languages, unit tests often test methods within a class.

Example:

class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance

    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
        else:
            raise ValueError("Deposit amount must be positive")

class TestBankAccount(unittest.TestCase):
    def test_deposit(self):
        account = BankAccount(100)
        account.deposit(50)
        self.assertEqual(account.balance, 150)

    def test_negative_deposit(self):
        account = BankAccount()
        with self.assertRaises(ValueError):
            account.deposit(-10)

if __name__ == '__main__':
    unittest.main()

Here we test whether deposits are processed correctly and whether a negative deposit generates an error message.

Example of a unit test

Let's look at a concrete example to understand how unit testing works in practice. We are using Python and the unit testing framework to test a simple function.

A simple unit test in Python

Suppose we have a function that checks whether a word is a palindrome (a word that remains the same if you flip it).

def is_palindrome(word):
    return word.lower() == word.lower()[::-1]

To test this function, we write a unit test:

import unittest

class TestPalindromeFunction(unittest.TestCase):
    def test_palindrome(self):
        self.assertTrue(is_palindrome("racecar"))
        self.assertTrue(is_palindrome("level"))
        self.assertFalse(is_palindrome("hello"))
    
    def test_case_insensitive(self):
        self.assertTrue(is_palindrome("Madam"))

    def test_empty_string(self):
        self.assertTrue(is_palindrome(""))

if __name__ == '__main__':
    unittest.main()

What happens here?

When we run this script, unittest checks that the function works correctly. If a test fails, we get an error message with details about what went wrong.

Extension: unit tests with parameters

Sometimes we want to test multiple input values without writing new test methods each time. For this we can use pytest with a parameterized test.

import pytest

@pytest.mark.parametrize("word, expected", [
    ("racecar", True),
    ("hello", False),
    ("Madam", True),
    ("", True)
])
def test_is_palindrome(word, expected):
    assert is_palindrome(word) == expected

With pytest.mark.parametrize, we can test the same function with different inputs in one test method. This makes the test code more uncluttered.

Advantages of unit testing

Unit testing offers numerous benefits that contribute to a more efficient development process and a more stable codebase. Below we discuss the most important benefits.

Efficient error detection

Unit testing helps developers detect bugs early in the development process. This prevents bugs from being discovered later in the process, where they are more expensive and time-consuming to fix.

Example:

Suppose you write a math function and later find out that dividing by zero is not handled correctly. A unit test can spot this problem immediately, so it doesn't move on to production.

Better documentation of code

Unit tests act as a kind of “living documentation” of code. New developers can use tests to understand how functions and methods are meant to work.

Example:

A test like this one shows directly what a function does and what input values are expected:

def add(a, b):
    return a + b

class TestMathOperations(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)  # Show that 2 + 3 should be 5

New team members can read this test and understand that add(a, b) adds numbers together.

Faster development cycles and more efficient debugging

Unit tests allow developers to change their code without fear of breaking existing functionality. This speeds up the development cycle and minimizes regression errors (bugs that reappear after a change).

Example:

Without unit tests, you would have to test manually, which takes much more time.

Suitable for Agile and DevOps

Unit testing fits perfectly within Agile and DevOps environments, where fast iterations and frequent releases are essential.

Many DevOps teams use CI/CD pipelines in which unit tests are automatically run with each code change. This prevents faulty code from entering production.

How developers use unit testing

Unit testing can be applied in several ways within the development process.

Test-driven development (TDD)

Test-driven development (TDD) is an approach in which tests are written before the actual code is implemented. The process consists of three phases:

Example of TDD:

Suppose we need a function that checks whether a number is even.

  1. Write the test:

    import unittest
    from mymodule import is_even
    
    class TestIsEven(unittest.TestCase):
        def test_even_numbers(self):
            self.assertTrue(is_even(2))
            self.assertTrue(is_even(4))
        
        def test_odd_numbers(self):
            self.assertFalse(is_even(3))
            self.assertFalse(is_even(5))
  2. Run the test: The test fails because is_even() is not yet implemented.

  3. Write the function:

    def is_even(n):
        return n % 2 == 0
  4. Run the test again: The test passes.

Using this approach, TDD ensures that functions do exactly what they are supposed to do, without unnecessary complexity.

Unit testing after completing code

Not all developers use TDD. Often, unit tests are written only after a block of code has been completed. This is done to verify that the function works correctly and continues to work with future changes.

Advantages:

Disadvantages:

Efficiency within DevOps and CI/CD

In DevOps environments, unit tests are often integrated into Continuous Integration (CI) pipelines. This means that with every change in the code, tests are automatically executed.

A typical CI/CD pipeline with unit tests includes:

Code commit → Unit tests run automatically → Code is deployed only if all tests pass

Example of a GitHub Actions workflow with unit tests:

name: Run Unit Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: 3.9
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run tests
        run: pytest

This ensures that every code change is tested immediately, so errors are detected early.

When is unit testing less useful?

While unit testing is a powerful technique, there are situations when it can be less effective or even impractical. Below we discuss some of these scenarios.

When time is a limiting factor

Unit testing takes time, especially with tight deadlines or rapidly changing projects. In some cases, it may be more practical to test only the most critical features instead of pursuing full test coverage.

Example:

Solution:

Strike a balance between test coverage and speed by testing only core functionality.

For UI/UX-focused applications

Unit tests are not suitable for testing user interfaces (UI) and user experiences (UX). This is because UI tests depend on visual elements and interactions that are difficult to validate with automated unit tests.

Example:

Solution:

Use UI test frameworks and user acceptance testing (UAT) in addition to unit tests.

Legacy codebases without tests

Legacy codebases often do not contain unit tests and may be poorly structured, making it difficult to add tests after the fact without significantly modifying the code.

Problems:

Solution:

With rapidly evolving requirements

When an application's specifications are constantly changing, unit tests can quickly become obsolete. This leads to additional maintenance work and lost time.

Example:

Solution:

Best practices for unit testing

To deploy unit testing effectively, it is important to follow some best practices. Below we discuss key guidelines for well-structured and efficient unit tests.

Use a unit test framework

Using a framework ensures structured and repeatable tests. Popular frameworks are:

With a framework, tests can be easily executed, structured and integrated into CI/CD pipelines.

Example of a test using PyTest:

import pytest

def multiply(a, b):
    return a * b

@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 6),
    (-1, 5, -5),
    (0, 10, 0)
])
def test_multiply(a, b, expected):
    assert multiply(a, b) == expected

Allows multiple test cases to be combined in one test.

Automate unit testing

Manual testing is error-prone and time-consuming. By automating unit tests in a CI/CD pipeline, new code can be tested immediately before it is merged.

Example of an automated test workflow with GitHub Actions:

name: Run Unit Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run tests
        run: pytest

Each time code is pushed, tests are run automatically.

Keep tests short and specific

Unit tests should test a single functionality. This makes it easier to locate errors and makes tests easier to maintain.

Bad example (tested too broadly):

def test_math_operations():
    assert add(2, 3) == 5
    assert multiply(2, 3) == 6
    assert divide(6, 3) == 2

If this test fails, it is unclear which function is causing the problem.

Good example (separated tests):

def test_add():
    assert add(2, 3) == 5

def test_multiply():
    assert multiply(2, 3) == 6

def test_divide():
    assert divide(6, 3) == 2

Each test focuses on one function, making it easier to find errors.

Keep tests independent

Each unit test should be independent of other tests. Tests that depend on external services or databases can lead to unreliable results.

Bad example (dependent on database):

def test_get_user():
    user = db.get_user(1)  # Database connection required
    assert user.name == "John Doe"

If the database is not available, the test fails.

Good example (use mocking):

from unittest.mock import Mock

def test_get_user():
    db = Mock()
    db.get_user.return_value = {"id": 1, "name": "John Doe"}
    assert db.get_user(1)["name"] == "John Doe"

By using a mock, the test remains reliable and independent of external systems.

Regular test execution.

Unit tests should be run not only at delivery, but regularly and with every change in the code. This prevents errors from going undetected.

Ways to automate this:

Unit testing versus other forms of testing

Unit testing is just one of many types of software testing. To get a good understanding of where unit testing fits into the bigger picture, we compare it to other popular testing methods.

Unit testing vs. QA testing

Important difference: Unit testing focuses on small units of code, while QA testing tests the software as a whole, including usability and edge cases.

Unit testing vs. functional testing

Important difference: Functional tests check whether the application works as expected from a user perspective, while unit tests look at the internal workings of code.

When do you use unit testing, and when do you use other forms of testing?

Use unit testing for:

Use QA and functional testing for:

Unit testing is essential

Unit testing is an indispensable part of software development. By testing small, individual units of code, it helps detect bugs early, improve code quality and speed up development cycles. It fits perfectly within Agile and DevOps processes and is a powerful tool for keeping software stable and maintainable.

While unit testing is not always the best solution – such as for UI/UX testing or rapidly changing code bases – it remains one of the most effective ways to make software more reliable. By applying the right strategies and best practices, unit testing can greatly improve overall software quality.

Want to learn more about unit testing or need help implementing it? Please don't hesitate to contact us and find out how we can help you with test automation and software quality.

Frequently Asked Questions
What is meant by unit testing?

Unit testing is a testing method in which individual components of software (such as functions or methods) are tested in isolation to verify that they work correctly. This is usually done with automated testing frameworks.


What is unit testing vs. QA testing?

Unit testing focuses on testing small units of code and is performed by developers. QA (Quality Assurance) testing checks the entire system and is often performed by a separate testing department.


What is unit testing vs. functional testing?

Unit testing tests individual functions or methods, while functional testing verifies that the software as a whole works correctly according to specifications. Functional testing mimics user interactions and tests complete workflows.


What are the 3 A's of unit testing?

The 3 A's of unit testing are: Arrange – Set up the test environment and initialize the required data. Act – Execute the function or method to be tested. Assert – Verify that the result matches the expectation.


sefa-senturk
Sefa Şentürk
Software Engineering Consultant

As a backend-focused software engineering consultant, I am dedicated to building robust, efficient, and scalable systems that power exceptional user experiences. I take pride in creating solid backend architectures, ensuring seamless integrations, and optimizing performance to meet the highest standards of reliability, functionality, and scalability.

Articles you might enjoy

Piqued your interest?

We'd love to tell you more.

Contact us
Tuple Logo
Veenendaal (HQ)
De Smalle Zijde 3-05, 3903 LL Veenendaal
info@tuple.nl‭+31 318 24 01 64‬
Quick Links
Customer Stories