Skip to content
Home / Fundamentals

Write Unit Tests

What Makes a Good Unit Test

A good unit test should be:

  • Isolated: Each test should be independent of other tests, and should not depend on any specific state or data.
  • Repeatable: The test should always produce the same result, regardless of when or where it is run.
  • Self-validating: The test should have a clear pass/fail result, and should not require manual interpretation or inspection.
  • Timely: The test should be written at the same time as the code it is testing, rather than being added after the fact.
  • Thorough: The test should cover as many cases and edge cases as possible.

Unit Testing in Python

Unit testing in Python can be done using the unittest module. This module provides a number of tools for writing and running tests, including the TestCase class, which provides methods for creating and running tests, and the assertEqual function, which allows you to check if a value is what you expect it to be.

Here is an example of a simple test using unittest:

import unittest

def add(x, y):
return x + y

class TestAdd(unittest.TestCase):
def test_add_two_positives(self):
result = add(2, 3)
self.assertEqual(result, 5)

    def test_add_two_negatives(self):
        result = add(-2, -3)
        self.assertEqual(result, -5)
        
    def test_add_positive_and_negative(self):
        result = add(2, -3)
        self.assertEqual(result, -1)

To run this test, you can use the unittest runner:

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

pytest

pytest is a popular testing framework for Python that makes it easier to write and run tests. One of the key benefits of pytest is that it allows you to write tests using standard Python syntax, rather than having to use special methods like unittest.

Here is the same test written using pytest:

def add(x, y):
    return x + y

def test_add_two_positives():
    result = add(2, 3)
    assert result == 5
    
def test_add_two_negatives():
    result = add(-2, -3)
    assert result == -5
    
def test_add_positive_and_negative():
    result = add(2, -3)
    assert result == -1

To run the tests using pytest, you can use the pytest command:

pytest test_add.py

Common Pitfalls to Avoid when Unit Testing

Not testing edge cases

It's important to test not just the expected input and output, but also edge cases that may be unexpected or unusual. For example, if you have a function that calculates the average of a list of numbers, you should test it with lists of different lengths, lists that contain negative numbers, and lists that contain zero or very large numbers.

Here is an example of how you might test a function that calculates the average of a list of numbers using pytest:

def test_calculate_average():
    assert calculate_average([1, 2, 3]) == 2
    assert calculate_average([1, -2, 3]) == 0
    assert calculate_average([]) == 0
    assert calculate_average([1e9, 1e9, 1e9]) == 1e9

Not testing error cases

You should also test cases where the function is expected to raise an exception, to ensure that it behaves correctly in these situations.

Here is an example of how you might test a function that raises an exception using pytest:

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
    divide(1, 0)

Testing implementation details

The goal of unit testing is to test the behavior of a piece of code, not its implementation. Therefore, it's important to avoid testing details of how the code is implemented, and focus on testing the inputs, outputs, and overall behavior of the code.

For example, suppose you have a function that reads a file and returns its contents as a string. You might be tempted to write a test that checks whether the function is using the correct file path or whether it is using the correct file reading method. However, these are implementation details that could change in the future, so it would be better to test the behavior of the function (i.e., whether it returns the correct contents of the file) rather than its implementation.

Here is an example of how you might test the behavior of a function that reads a file, rather than its implementation:

def test_read_file():
    result = read_file('test.txt')
    assert result == 'Hello, world!\n'

Writing tests after the code: As mentioned earlier, it's important to write tests at the same time as the code, rather than adding them after the fact. This helps ensure that the tests cover all of the necessary cases, and that the code is designed with testing in mind.

Best Practices when Writing an Unit Test

  • Keep tests small and focused: Each test should test a single concept or behavior, rather than trying to test multiple things at once. This makes it easier to understand what the test is doing, and makes it easier to troubleshoot if the test fails.
  • Use descriptive names for tests: Good test names should clearly describe what the test is doing, and should include enough detail to understand what is being tested without having to read the test itself.
  • Use fixtures to reduce duplication: pytest allows you to define "fixtures" that can be used to set up common test data or environments. This can help reduce duplication in your tests, and make it easier to add or modify tests.
  • Use parameterized tests to test multiple cases: pytest also allows you to write parameterized tests, which allow you to test the same code with multiple sets of input data. This can be a useful way to reduce duplication and test a wider range of cases.