Python's unittest: Writing Unit Tests for Your Code

Python's unittest: Writing Unit Tests for Your Code

by Leodanis Pozo Ramos Apr 29, 2024 intermediate testing

The Python standard library ships with a testing framework named unittest, which you can use to write automated tests for your code. The unittest package has an object-oriented approach where test cases derive from a base class, which has several useful methods.

The framework supports many features that will help you write consistent unit tests for your code. These features include test cases, fixtures, test suites, and test discovery capabilities.

In this tutorial, you’ll learn how to:

  • Write unittest tests with the TestCase class
  • Explore the assert methods that TestCase provides
  • Use unittest from the command line
  • Group test cases using the TestSuite class
  • Create fixtures to handle setup and teardown logic

To get the most out of this tutorial, you should be familiar with some important Python concepts, such as object-oriented programming, inheritance, and assertions. Having a good understanding of code testing is a plus.

Take the Quiz: Test your knowledge with our interactive “Python's unittest: Writing Unit Tests for Your Code” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Python's unittest: Writing Unit Tests for Your Code

In this quiz, you'll test your understanding of Python testing with the unittest framework from the standard library. With this knowledge, you'll be able to create basic tests, execute them, and find bugs before your users do.

Testing Your Python Code

Code testing or software testing is a fundamental part of a modern software development cycle. Through code testing, you can verify that a given software project works as expected and fulfills its requirements. Testing enforces code quality and robustness.

You’ll do code testing during the development stage of an application or project. You’ll write tests that isolate sections of your code and verify its correctness. A well-written battery or suite of tests can also serve as documentation for the project at hand.

You’ll find several different concepts and techniques around testing. Most of them surpass the scope of this tutorial. However, unit test is an important and relevant concept. A unit test is a test that operates on an individual unit of software. A unit test aims to validate that the tested unit works as designed.

A unit is often a small part of a program that takes a few inputs and produces an output. Functions, methods, and other callables are good examples of units that you’d need to test.

In Python, there are several tools to help you write, organize, run, and automate your unit test. In the Python standard library, you’ll find two of these tools:

  1. doctest
  2. unittest

Python’s doctest module is a lightweight testing framework that provides quick and straightforward test automation. It can read the test cases from your project’s documentation and your code’s docstrings. This framework is shipped with the Python interpreter as part of the batteries-included philosophy.

The unittest package is also a testing framework. However, it provides a more complete solution than doctest. In the following sections, you’ll learn and work with unittest to create suitable unit tests for your Python code.

Getting to Know Python’s unittest

The unittest package provides a unit test framework inspired by JUnit, which is a unit test framework for the Java language. The unittest framework is directly available in the standard library, so you don’t have to install anything to use this tool.

The framework uses an object-oriented approach and supports some essential concepts that facilitate test creation, organization, preparation, and automation:

  • Test case: An individual unit of testing. It examines the output for a given input set.
  • Test suite: A collection of test cases, test suites, or both. They’re grouped and executed as a whole.
  • Test fixture: A group of actions required to set up an environment for testing. It also includes the teardown processes after the tests run.
  • Test runner: A component that handles the execution of tests and communicates the results to the user.

In the following sections, you’ll dive into using the unittest package to create test cases, suites of tests, fixtures, and, of course, run your tests.

Organizing Your Tests With the TestCase Class

The unittest package defines the TestCase class, which is primarily designed for writing unit tests. To start writing your test cases, you just need to import the class and subclass it. Then, you’ll add methods whose names should begin with test. These methods will test a given unit of code using different inputs and check for the expected results.

Here’s a quick test case that tests the built-in abs() function:

Python
import unittest

class TestAbsFunction(unittest.TestCase):
    def test_positive_number(self):
        self.assertEqual(abs(10), 10)

    def test_negative_number(self):
        self.assertEqual(abs(-10), 10)

    def test_zero(self):
        self.assertEqual(abs(0), 0)

The abs() function takes a number as an argument and returns its absolute value. In this test case, you have three test methods. Each method checks for a specific input and output combination.

To create the test case, you subclass the TestCase class and add three methods. The first method checks whether abs() returns the correct value when you pass a positive number. The second method checks the expected behavior with a negative number. Finally, the third method checks the return value of abs() when you use 0 as an argument.

Note that to check the conditions, you use the .assertEqual() method, which your class inherits from TestCase. More on these types of methods in a moment. For now, you’re nearly ready to write and run your first test case with unittest.

Creating Test Cases

Before you write tests with unittest, you need some code to test. Suppose that you need to get a person’s age, process that information, and display their current life stage. For example, if the person’s age is:

  • Between 0 and 9, both included, the function should return "Child".
  • Greater than 9 and less than or equal to 18, the function should return "Adolescent".
  • Greater than 18 and less than or equal to 65, the function should return "Adult".
  • Greater than 65 and less than or equal to 150, the function should return "Golden age".
  • Negative or greater than 150, the function should return "Invalid age".

In this situation, you can write a function like the following:

Python age.py
def categorize_by_age(age):
    if 0 <= age <= 9:
        return "Child"
    elif 9 < age <= 18:
        return "Adolescent"
    elif 18 < age <= 65:
        return "Adult"
    elif 65 < age <= 150:
        return "Golden age"
    else:
        return f"Invalid age: {age}"

This function should return correct results with different age values. To make sure that the function works correctly, you can write some unittest tests.

Following the pattern from the previous section, you’ll start by subclassing TestCase and add some methods that will help you test different input values and the corresponding results:

Python test_age.py
import unittest

from age import categorize_by_age

class TestCategorizeByAge(unittest.TestCase):
    def test_child(self):
        self.assertEqual(categorize_by_age(5), "Child")

    def test_adolescent(self):
        self.assertEqual(categorize_by_age(15), "Adolescent")

    def test_adult(self):
        self.assertEqual(categorize_by_age(30), "Adult")

    def test_golden_age(self):
        self.assertEqual(categorize_by_age(70), "Golden age")

    def test_negative_age(self):
        self.assertEqual(categorize_by_age(-1), "Invalid age: -1")

    def test_too_old(self):
        self.assertEqual(categorize_by_age(151), "Invalid age: 151")

In this example, you create a subclass of TestCase with the descriptive name TestCategorizeByAge. Note that the class name starts with Test, which is a widely used convention to make the purpose of the class immediately clear to anyone reading your code.

Also, note that the containing file is called test_age.py. By default, unittest supports test discovery based on the name of test modules. The default naming pattern is test*.py. Here, the asterisk (*) represents any sequence of characters, so starting your modules with test is recommended if you want to take advantage of the default test discovery configuration.

Then, you define six methods. Each method tests for an input value and the expected result. The methods use the .assertEqual() method from the parent class to check whether the function’s output equals the expected value.

Note that the tests above check every possible branch in the categorize_by_age() function. However, they don’t cover the boundary cases where the input age is the lower or upper limit of the interval. To make sure that the function responds as expected in those cases, you can add the following tests:

Python test_age.py
# ...

class TestCategorizeByAge(unittest.TestCase):
    # ...

    def test_boundary_child_adolescent(self):
        self.assertEqual(categorize_by_age(9), "Child")
        self.assertEqual(categorize_by_age(10), "Adolescent")

    def test_boundary_adolescent_adult(self):
        self.assertEqual(categorize_by_age(18), "Adolescent")
        self.assertEqual(categorize_by_age(19), "Adult")

    def test_boundary_adult_golden_age(self):
        self.assertEqual(categorize_by_age(65), "Adult")
        self.assertEqual(categorize_by_age(66), "Golden age")

These test methods have two assertions each. The first assertion checks for the upper limit of the age interval, and the second assertion checks for the lower limit of the next age interval.

Using multiple assertions in a test method helps you reduce boilerplate code. For example, if you use a single assertion to write these tests, then you’ll have to write six test methods instead of just three. Each method will need a unique name, which can be a challenge.

In general, using multiple assertions in your test methods has the following pros:

  • Efficiency: Multiple assertions in a single test can reduce repetitive code. It can also make tests run faster in those scenarios where you have setup and teardown requirements for each test.
  • Contextual testing: Multiple assertions might be necessary to check that a function behaves correctly in a specific context.
  • Convenience: Multiple assertions in a test can be more straightforward and less tedious to write compared to writing multiple single-assertion tests.

The approach also has its cons:

  • Clarity and isolation: When a test with multiple assertions fails, it can be harder to immediately identify which assertion caused the failure. This can go against your debugging process.
  • Breakage risk: When an early assertion in a test fails, subsequent assertions are not executed. This can hide additional issues.
  • Test purpose blurring: When a test has multiple assertions, it can become less focused. This can make the test harder to understand.

With the test cases in place, you’re ready to run them and see whether your categorize_by_age() function works as expected.

Running unittest Tests

Once you’ve written the tests, you need a way to run them. You’ll have at least two standard ways to run tests with unittest:

  1. Make the test module executable
  2. Use the command-line interface of unittest

To make a test module executable in unittest, you can add the following code to the end of the module:

Python test_age.py
# ...

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

The main() function from unittest allows you to load and run a set of tests. You can also use this function to make the test module executable. Once you’ve added these lines of code, you can run the module as a regular Python script:

Shell
$ python test_age.py
.........
----------------------------------------------------------------------
Ran 9 tests in 0.000s

OK

This command runs the tests from test_age.py. In the output, every dot represents a passing test. Then, you have a quick summary of the number of run tests and the execution time. All the tests passed, so you get an OK at the end of the output.

Among other arguments, the main() function takes the verbosity one. With this argument, you can tweak the output’s verbosity, which has three possible values:

  • 0 for quiet
  • 1 for normal
  • 2 for detailed

Go ahead and update the call to main() as in the following snippet:

Python test_age.py
# ...

if __name__ == "__main__":
    unittest.main(verbosity=2)

In the highlighted line, you set the verbosity level to 2. This update makes unittest generate a more detailed output when you run the test module:

Shell
$ python test_age.py
test_adolescent (__main__.TestCategorizeByAge.test_adolescent) ... ok
test_adult (__main__.TestCategorizeByAge.test_adult) ... ok
test_boundary_adolescent_adult (__main__.TestCategorizeByAge.test_boundary_adolescent_adult) ... ok
test_boundary_adult_golden_age (__main__.TestCategorizeByAge.test_boundary_adult_golden_age) ... ok
test_boundary_child_adolescent (__main__.TestCategorizeByAge.test_boundary_child_adolescent) ... ok
test_child (__main__.TestCategorizeByAge.test_child) ... ok
test_golden_age (__main__.TestCategorizeByAge.test_golden_age) ... ok
test_negative_age (__main__.TestCategorizeByAge.test_negative_age) ... ok
test_too_old (__main__.TestCategorizeByAge.test_too_old) ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.000s

OK

This output is more detailed. It shows the tests and their result. At the end, it summarizes the test run as usual.

If you want to make the detailed output more descriptive, then you can add docstrings to your tests like in the following code snippet:

Python test_age.py
# ...

class TestCategorizeByAge(unittest.TestCase):
    def test_child(self):
        """Test for 'Child'"""
        self.assertEqual(categorize_by_age(5), "Child")

    def test_adolescent(self):
        """Test for 'Adolescent'"""
        self.assertEqual(categorize_by_age(15), "Adolescent")

    def test_adult(self):
        """Test for 'Adult'"""
        self.assertEqual(categorize_by_age(30), "Adult")

    def test_golden_age(self):
        """Test for 'Golden age'"""
        self.assertEqual(categorize_by_age(70), "Golden age")

    def test_negative_age(self):
        """Test for negative age"""
        self.assertEqual(categorize_by_age(-1), "Invalid age: -1")

    def test_too_old(self):
        """Test for too old"""
        self.assertEqual(categorize_by_age(151), "Invalid age: 151")

    def test_boundary_child_adolescent(self):
        """Test for boundary between 'Child' and 'Adolescent'"""
        self.assertEqual(categorize_by_age(9), "Child")
        self.assertEqual(categorize_by_age(10), "Adolescent")

    def test_boundary_adolescent_adult(self):
        """Test for boundary between 'Adolescent' and 'Adult'"""
        self.assertEqual(categorize_by_age(18), "Adolescent")
        self.assertEqual(categorize_by_age(19), "Adult")

    def test_boundary_adult_golden_age(self):
        """Test for boundary between 'Adult' and 'Golden age'"""
        self.assertEqual(categorize_by_age(65), "Adult")
        self.assertEqual(categorize_by_age(66), "Golden age")

# ...

In this update of your test methods, you add human-readable docstrings. Now, when you run the test with a verbosity of 2, you get the following output:

Shell
$ python test_age.py
test_adolescent (__main__.TestCategorizeByAge.test_adolescent)
Test for 'Adolescent' ... ok
test_adult (__main__.TestCategorizeByAge.test_adult)
Test for 'Adult' ... ok
test_boundary_adolescent_adult (__main__.TestCategorizeByAge.test_boundary_adolescent_adult)
Test for boundary between 'Adolescent' and 'Adult' ... ok
test_boundary_adult_golden_age (__main__.TestCategorizeByAge.test_boundary_adult_golden_age)
Test for boundary between 'Adult' and 'Golden age' ... ok
test_boundary_child_adolescent (__main__.TestCategorizeByAge.test_boundary_child_adolescent)
Test for boundary between 'Child' and 'Adolescent' ... ok
test_child (__main__.TestCategorizeByAge.test_child)
Test for 'Child' ... ok
test_golden_age (__main__.TestCategorizeByAge.test_golden_age)
Test for 'Golden age' ... ok
test_negative_age (__main__.TestCategorizeByAge.test_negative_age)
Test for negative age ... ok
test_too_old (__main__.TestCategorizeByAge.test_too_old)
Test for too old ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.000s

OK

Now, apart from the odd function names, unittest uses the docstrings to improve the output’s readability, which is great.

Skipping Tests

The unittest framework also supports skipping individual test methods and even whole test case classes. Skipping tests allows you to temporarily bypass a test case without permanently removing it from your test suite.

Here are some common situations where you may need to skip tests:

  • Incomplete feature: When you have an incomplete feature, you’ll need to skip the related tests to avoid false negatives.
  • External services: When some of your tests depend on external services or resources that aren’t available, you may need to skip them until the service is back.
  • Conditional execution: When you have tests that depend on specific conditions, such as a platform or a Python version, then you can conditionally skip the tests based on dynamic conditions.
  • Known failures: When your code has a known bug, you might skip the failing test with a reference to the bug report.
  • Performance considerations: When you have tests that are time-consuming or resource-intensive, you might want to skip them during regular development cycles to speed up the testing process.
  • Deprecated features: When you have deprecated features that haven’t been removed yet, you can skip their related tests until the feature is removed entirely.

The following decorators will help you with the goal of skipping tests during your test running process:

Decorator Description
@unittest.skip(reason) Skips the decorated test
@unittest.skipIf(condition, reason) Skips the decorated test if condition is true
@unittest.skipUnless(condition, reason) Skips the decorated test unless condition is true

In these decorators, the reason argument should describe why the test will be skipped. Consider the following toy example that shows how the decorators work:

Python
import sys
import unittest

class SkipTestExample(unittest.TestCase):
    @unittest.skip("Unconditionally skipped test")
    def test_unimportant(self):
        self.fail("The test should be skipped")

    @unittest.skipIf(sys.version_info < (3, 12), "Requires Python >= 3.12")
    def test_using_calendar_constants(self):
        import calendar

        self.assertEqual(calendar.Month(10), calendar.OCTOBER)

    @unittest.skipUnless(sys.platform.startswith("win"), "Requires Windows")
    def test_windows_support(self):
        from ctypes import WinDLL, windll

        self.assertIsInstance(windll.kernel32, WinDLL)

if __name__ == "__main__":
    unittest.main(verbosity=2)

In this sample test module, you have three test methods. The first one never runs because you use the @skip decorator on it. The second test method only runs if you’re on a Python version equal to or greater than 3.12. Finally, the last test method runs if you’re on a Windows box.

Here’s how the output looks on Windows with Python 3.11:

Windows PowerShell
PS> python skip_tests.py
test_unimportant (__main__.SkipTestExample.test_unimportant) ... skipped 'Unconditionally skipped test'
test_using_calendar_constants (__main__.SkipTestExample.test_using_calendar_constants) ... skipped 'Requires Python >= 3.12'
test_windows_support (__main__.SkipTestExample.test_windows_support) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.156s

OK (skipped=2)

In this case, the only test that runs is the last one because you’re on Windows. The first test doesn’t run because of the @skip decorator, and the second test doesn’t run because the Python version is less than 3.12.

Here’s the output of these test in macOS or Linux with Python 3.12:

Shell
$ python skip_tests.py
test_unimportant (__main__.SkipTestExample.test_unimportant) ... skipped 'Unconditionally skipped test'
test_using_calendar_constants (__main__.SkipTestExample.test_using_calendar_constants) ... ok
test_windows_support (__main__.SkipTestExample.test_windows_support) ... skipped 'Requires Windows'

----------------------------------------------------------------------
Ran 3 tests in 0.002s

OK (skipped=2)

In this test run, the first test doesn’t run. The second test runs because the Python version is the expected. The final test doesn’t run because the current platform isn’t Windows.

Creating Subtests

The unittest framework allows you to distinguish between similar tests using the subTest() context manager. For example, say that you have a function that checks whether a given number is even:

Python even.py
def is_even(number):
    return number % 2 == 0

This function uses the modulo operator to check whether the input number is even. Here are some basic tests for the function:

Python
import unittest

from even import is_even

class TestIsEven(unittest.TestCase):
    def test_even_number(self):
        self.assertEqual(is_even(2), True)

    def test_odd_number(self):
        self.assertEqual(is_even(3), False)

    def test_negative_even_number(self):
        self.assertEqual(is_even(-2), True)

    def test_negative_odd_number(self):
        self.assertEqual(is_even(-3), False)

if __name__ == "__main__":
    unittest.main(verbosity=2)

These tests take positive and negative numbers and check whether the function’s result is correct by comparing it with the appropriate Boolean value. However, you’ve only tested a couple of input values. If you want to expand the input dataset, then you can use subtests.

To create a subtest, you’ll use the .subTest() method, which returns a context manager that executes the enclosed code block as a subtest. You can use this context manager to provide multiple input values for your tests.

Here’s the above example using subtests to check for multiple numbers:

Python
import unittest

from even import is_even

class TestIsEven(unittest.TestCase):
    def test_even_number(self):
        for number in [2, 4, 6, -8, -10, -12]:
            with self.subTest(number=number):
                self.assertEqual(is_even(number), True)

    def test_odd_number(self):
        for number in [1, 3, 5, -7, -9, -11]:
            with self.subTest(number=number):
                self.assertEqual(is_even(number), False)

if __name__ == "__main__":
    unittest.main(verbosity=2)

In this example, you use a for loop that iterates over a list of input values. Then, you use the with statement to manage the context that .subTest() constructs. In that context, you run the assertion with the current number, which works as a subtest. Go ahead and run the test to check the results.

Exploring the Available Assert Methods

As you’ve read in previous sections, the TestCase class provides a set of assert methods. You can use these methods to check multiple conditions while writing your tests. You’ll find over twenty methods in total. They let you compare single values, such as numbers and Booleans, and collections, such as lists, tuples, dictionaries, and more.

You’ll also find a few methods that you can use to check for those situations where your code raises exceptions. You’ll learn about all these methods in the following sections.

Comparing Values

Comparing the result of a code unit with the expected value is a common way to check whether the unit works okay. The TestCase class defines a rich set of methods that allows you to do this type of check:

Method Comparison
.assertEqual(a, b) a == b
.assertNotEqual(a, b) a != b
.assertTrue(x) bool(x) is True
.assertFalse(x) bool(x) is False

You’ve already seen a couple of examples that use .assertEqual(). The .assertNotEqual() method works similarly but with the opposite logic.

To illustrate how the .assertTrue() and .assertFalse() methods work, say that you have the following function:

Python prime_v1.py
import math

def is_prime(number):
    if number <= 1:
        return False
    for i in range(2, int(math.sqrt(number)) + 1):
        if number % i == 0:
            return False
    return True

This is a Boolean-valued function, also known as a predicate function, that returns True if the input number is prime and False otherwise. Here’s how you can test this function with unittest:

Python test_prime_v1.py
import unittest

from prime_v1 import is_prime

class TestIsPrime(unittest.TestCase):
    def test_prime_number(self):
        self.assertTrue(is_prime(17))

    def test_non_prime_number(self):
        self.assertFalse(is_prime(10))

if __name__ == "__main__":
    unittest.main(verbosity=2)

In this example, you have a TestIsPrime class with two methods. The first method tests is_prime() with a prime number, which must result in True, so you use .assertTrue() for the check. The second method tests is_prime() with a non-prime number, which must result in False, and you use .assertFalse() in this case.

If you run the script, then you get the following output:

Shell
$ python test_prime_v1.py
test_non_prime_number (__main__.TestIsPrime.test_non_prime_number) ... ok
test_prime_number (__main__.TestIsPrime.test_prime_number) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Both tests pass successfully. So, your is_prime() function works okay. In this example, the number of tests is quite small, so you can take advantage of subtests to run is_prime() with several different numbers.

Comparing Objects by Their Identity

TestCase also implements methods that are related to the identity of objects. In Python’s CPython implementation, an object’s identity is the memory address where the object lives. This identity is a unique identifier that distinguishes one object from another.

An object’s identity is a read-only property, which means that you can’t change an object’s identity once you’ve created the object. To check an object’s identity, you’ll use the is and is not operators.

Here are a few assert methods that help you check for an object’s identity:

Method Comparison
.assertIs(a, b) a is b
.assertIsNot(a, b) a is not b
.assertIsNone(x) x is None
.assertIsNotNone(x) x is not None

As you can conclude from this table, these methods are shortcuts for the is and is not operators, as suggested by the method names. With the first two methods, you can compare two objects between them. With the final two methods, you can compare an object against None, which is the Python null value.

Consider the following toy example that checks for the identity of list objects:

Python test_identity.py
import unittest

class TestListIdentity(unittest.TestCase):
    def test_list_aliases(self):
        a = ["Python", "unittest"]
        b = a
        self.assertIs(a, b)

    def test_list_objects(self):
        a = ["Python", "unittest"]
        b = ["Python", "unittest"]
        self.assertIsNot(a, b)

if __name__ == "__main__":
    unittest.main(verbosity=2)

In the first test, you create a list containing strings. Then, you derive an alias from the original list. Aliases of an object hold a reference to the same object. So, when you compare them with .assertIs(), your check succeeds because both variables refer to the same object with the same identity.

In the second test, you create two different and independent list objects with the same data. These lists don’t point to the same object in memory, so they have different identities. In this example, you use the .assertIsNot() methods, which should succeed because the compared objects don’t have the same identity.

Go ahead and run the tests:

Shell
$ python test_identity.py
test_list_aliases (__main__.TestListIdentity.test_list_aliases) ... ok
test_list_objects (__main__.TestListIdentity.test_list_objects) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

When you run the test_identity.py file, you’ll see that both tests pass. Identity tests like this one are useful when you’re testing a function or method that must return cached or singleton objects.

Comparing Collections

Another common need when writing tests is to compare collections, such as lists, tuples, strings, dictionaries, and sets. The TestCase class also has shortcut methods for these types of comparisons. Here’s a summary of those methods:

Method Comparison
.assertSequenceEqual(a, b) Equality of two sequences
.assertMultiLineEqual(a, b) Equality of two strings
.assertListEqual(a, b) Equality of two lists
.assertTupleEqual(a, b) Equality of two tuples
.assertDictEqual(a, b) Equality of two dictionaries
.assertSetEqual(a, b) Equality of two sets

These methods run equality tests between different collection types. Unlike the methods in the previous section, these compare the values of objects rather than their identities.

Here’s a quick example that showcases several of these methods:

Python test_collections.py
import unittest

class TestCollections(unittest.TestCase):
    def test_sequence_objects(self):
        a = ("H", "e", "l", "l", "o")
        b = "Hello"
        self.assertSequenceEqual(a, b)

    def test_string_objects(self):
        a = "Hello"
        b = "Hello"
        self. assertMultiLineEqual(a, b)

    def test_list_objects(self):
        a = [1, 2, 3, 4, 5]
        b = [1, 2, 3, 4, 5]
        self.assertListEqual(a, b)

    def test_tuple_objects(self):
        a = ("Jane", 25, "New York")
        b = ("Jane", 25, "New York")
        self.assertTupleEqual(a, b)

    def test_dictionary_objects(self):
        a = {"framework": "unittest", "language": "Python"}
        b = {"language": "Python", "framework": "unittest"}
        self.assertDictEqual(a, b)

    def test_set_objects(self):
        a = {1, 2, 4, 3, 5}
        b = {1, 5, 3, 4, 2}
        self.assertSetEqual(a, b)

if __name__ == "__main__":
    unittest.main(verbosity=2)

In this example, you have several toy tests. The first test compares a tuple with a string using the .assertSequenceEqual() method because they’re both sequences. The test passes because both sequences contain the same set of characters. Note that you can’t perform this test with the .assertEqual() method, so this is a real specialized use case.

Next, you write similar tests to compare string, list, tuple, dictionary, and set objects. It’s important to highlight that the .assertDictEqual() and .assertSetEqual() methods compare their target objects using the same rules for regular comparisons between dictionaries and sets. They compare the items without considering their order in the collection.

Now, go ahead and run the test_collections.py file from your command line to check the results.

Running Membership Tests

A membership test is a check that allows you to determine whether a given value is or is not in a collection of values. You’ll run these tests with the in and not in operators. Again, the TestCase class has methods for these types of checks:

Method Check
.assertIn(a, b) a in b
.assertNotIn(a, b) a not in b

These two methods provide shortcuts for you to run membership tests on your code. Here’s a quick example of how they work:

Python test_membership.py
import unittest

class TestMembership(unittest.TestCase):
    def test_value_in_collection(self):
        a = 1
        b = [1, 2, 3, 4, 5]
        self.assertIn(a, b)

    def test_value_not_in_collection(self):
        a = 10
        b = [1, 2, 3, 4, 5]
        self.assertNotIn(a, b)

if __name__ == "__main__":
    unittest.main(verbosity=2)

In the first test, you check whether a is in the list of values, b. To do this, you use the .assertIn() method. In the second test, you use .assertNotIn() to check whether the a value is not in the target list, b.

Checking for an Object’s Type

Checking the type of the object that a function, method, or callable returns may be another common requirement in testing. For this purpose, the TestClass also has dedicated assert methods:

Method Comparison
.assertIsInstance(a, b) isinstance(a, b)
.assertNotIsInstance(a, b) not isinstance(a, b)

These two methods are based on the built-in isinstance() function, which you can use to check whether the input object is of a given type.

As an example, say that you have the following class hierarchy:

Python vehicles.py
class Vehicle:
    def __init__(self, make, model):
        self.make = make
        self.model = model

class Car(Vehicle):
    def __init__(self, make, model, max_speed):
        super().__init__(make, model)
        self.max_speed = max_speed

class Truck(Vehicle):
    def __init__(self, make, model, loading_capacity):
        super().__init__(make, model)
        self.loading_capacity = loading_capacity

def vehicle_factory(cls, *args, **kwargs):
    return cls(*args, **kwargs)

In this example, you have a Vehicle class that is at the top of your class hierarchy. Then, you have two concrete classes that inherit from Vehicle and extend it with new attributes. Finally, you have a factory function that you’ll use to create instances of your classes.

To test this code with unittest, you can do something like the following:

Python test_vehicles.py
import unittest

import vehicles

class TestVehicles(unittest.TestCase):
    def test_vehicle_is_instance_of_car(self):
        car = vehicles.Car("Chevrolet", "Corvette", 194)
        self.assertIsInstance(car, vehicles.Car)

    def test_vehicle_is_instance_of_truck(self):
        truck = vehicles.Truck("Ford", "F-150", 2000)
        self.assertIsInstance(truck, vehicles.Truck)

    def test_vehicles_factory(self):
        car = vehicles.vehicle_factory(
            vehicles.Car,
            make="Toyota",
            model="Corolla",
            max_speed=180,
        )
        self.assertIsInstance(car, vehicles.Vehicle)

if __name__ == "__main__":
    unittest.main(verbosity=2)

In the first two tests, you use .assertIsInstance() to check whether the current object is an instance of Car and Truck, respectively. In the final test, you check the vehicle_factory() function. In this case, you use the base class as the comparison type.

Testing for Exceptions

Sometimes, you’ll need to check for exceptions. Yes, sometimes your own code will raise exceptions as part of its behavior. The TestCase class also provides assert methods that allow you to check for exceptions:

Method Check
.assertRaises(exc, fun, *args, **kwds) fun(*args, **kwds) raises exc
.assertRaisesRegex(exc, r, fun, *args, **kwds) fun(*args, **kwds) raises exc and the message matches regex r

The first method allows checking for explicit exceptions without considering the associated error message, and the second method checks for exceptions and considers the associated message using regular expressions.

To illustrate how you can use these methods in your testing code, consider the following reimplementation of your is_prime() function:

Python prime_v2.py
from math import sqrt

def is_prime(number):
    if not isinstance(number, int):
        raise TypeError(
            f"integer number expected, got {type(number).__name__}"
        )
    if number < 2:
        raise ValueError(f"integer above 1 expected, got {number}")
    for candidate in range(2, int(sqrt(number)) + 1):
        if number % candidate == 0:
            return False
    return True

In this updated version of is_prime(), you have two conditional statements. The first conditional ensures that the input number is an integer. If that’s not the case, then your code raises a TypeError with an appropriate error message.

The second conditional checks for input numbers greater than 2. If the input number is 1 or lower, then you raise a ValueError. The rest of the code is similar to what you’ve already seen.

How would you write tests to check that the function raises the appropriate exceptions? To do this, you can use the .assertRaises() method:

Python test_prime_v2.py
import unittest

from prime_v2 import is_prime

class TestIsPrime(unittest.TestCase):
    def test_prime_number(self):
        self.assertTrue(is_prime(17))

    def test_non_prime_number(self):
        self.assertFalse(is_prime(10))

    def test_invalid_type_float(self):
        with self.assertRaises(TypeError):
            is_prime(4.5)

    def test_invalid_type_str(self):
        with self.assertRaises(TypeError):
            is_prime("5")

    def test_zero_and_one(self):
        with self.assertRaises(ValueError):
            is_prime(0)
        with self.assertRaises(ValueError):
            is_prime(1)

    def test_negative_number(self):
        with self.assertRaises(ValueError):
            is_prime(-1)

if __name__ == "__main__":
    unittest.main(verbosity=2)

The first two tests are familiar. They cover the primary use case of your is_prime() function. The third and fourth tests check for those cases where the function gets an argument of an incorrect type. These tests specifically check for floating-point numbers and strings.

The fifth test checks for those situations where the input is 0 or 1. In those cases, the function must raise a ValueError, so that’s what the assertions catch. Finally, the sixth test checks for negative numbers, which must also raise a ValueError. You can run the test from your command line to check how they work.

The TestCase class also provides some additional assert methods that help you with warnings and logs:

Method Check
.assertWarns(warn, fun, *args, **kwds) fun(*args, **kwds) raises warn
.assertWarnsRegex(warn, r, fun, *args, **kwds) fun(*args, **kwds) raises warn and the message matches regex r
.assertLogs(logger, level) The with block logs on logger with minimum level
.assertNoLogs(logger, level) The with block does not log on logger with minimum level

The first two methods allow you to check for warnings, which are a special category of exceptions in Python. A common example of a warning is a DeprecationWarning, which appears when you use deprecated features of the language.

The final two methods allow you to handle logging. These methods return context managers to test whether a message is logged on the logger or one of its children, with at least the given level.

Using Custom Assert Methods

As with many things in Python, you can also create your assert methods to facilitate your test writing process. To do this, you can subclass TestCase and extend the class with new assertion methods.

For example, say that you frequently need to check that all the values in a list are integer numbers. In that case, you can create a test case class like the following:

Python test_custom.py
import unittest

class CustomTestCase(unittest.TestCase):
    def assertAllIntegers(self, values):
        for value in values:
            self.assertIsInstance(
                value,
                int,
            )

This class inherits from TestCase. So, it provides all the assert methods you’ve seen so far. In addition, you extend the functionality of TestCase with a new assert method called .assertAllIntegers(), which takes a list of values and checks whether all the values are integer numbers.

Here’s how you can use this class in practice:

Python
import unittest

class CustomTestCase(unittest.TestCase):
    # ...

class TestIntegerList(CustomTestCase):
    def test_values_are_integers(self):
        integers_list = [1, 2, 3, 4, 5]
        self.assertAllIntegers(integers_list)

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

In this class, you define a test that uses the .assertAllIntegers() method to test a list where all the values are integers. This test must pass successfully. You can experiment with other lists where the values don’t have the correct type to see how the CustomTestCase class works.

Using unittest From the Command Line

The unittest package also provides a command-line interface (CLI) that you can use to discover and run your tests. With this interface, you can run tests from modules, classes, and even individual test methods.

In the following sections, you’ll learn the basics of how to use the unittest framework’s CLI to discover and run your tests.

Running Tests

With the command-line interface of unittest, you can run tests directly from modules, classes, and from individual test methods. The following sample commands cover those use cases:

Shell
$ python -m unittest test_module1 test_module2
$ python -m unittest test_module.TestCase
$ python -m unittest test_module.TestCase.test_method

The first command will run all the tests from test_module1 and test_module2. You can add more modules to the list if you need to. The second command allows you to run all the tests from a given TestCase class. Finally, the last command lets you run a specific test method from a given TestCase class.

As an example, run the following command to execute the tests in your test_age.py file:

Shell
$ python -m unittest test_age
.........
----------------------------------------------------------------------
Ran 9 tests in 0.000s

OK

Note that for this command to work, the target module must be available in the import path of your current Python environment. Otherwise, you’ll get an import error. To avoid this issue, you can also run the tests by passing in the module’s file path:

Shell
$ python -m unittest test_age.py
.........
----------------------------------------------------------------------
Ran 9 tests in 0.000s

OK

With this command, unittest will find and load the test module from the provided path, which saves you from getting an import error.

Discovering Tests Automatically

The unittest framework supports test discovery. The test loader can inspect each module in a given directory looking for classes derived from TestCase. Then, the loader groups the found classes within a complete test suite.

For example, to discover and run all the tests that you’ve written so far, run the following command in the directory that contains them:

Shell
$ python -m unittest discover
...................................
----------------------------------------------------------------------
Ran XX tests in 0.001s

OK

This command locates all tests in the current directory, groups them in a test suite, and finally runs them. You can use the python -m unittest as a shortcut for the above command.

You can use the -s or --start-directory command-line options with the discover subcommand to specify the directory where your tests reside. Other command-line options of discover include:

Option Description
-v, --verbose Produces a verbose output
-p, --pattern Allows for using glob patterns and defaults to test*.py
-t, --top-level-directory Defines the top-level directory of a project

With these command-line options, you can tweak your test discovery process to fulfill your test automation needs.

Using Command-Line Options

You’ll also find that the unittest CLI supports several command-line options. Here’s a summary of them:

Option Description
-v Shows a more verbose output
-b, --buffer Buffers the standard output and error streams during the test execution
-c, --catch Waits for the current test to run and reports all the results up to the point Ctrl+C is pressed during the test execution
-f, --failfast Stops the test run on the first error or failure
-k Only runs test methods and classes that match the pattern or substring
--locals Shows local variables in tracebacks
--durations N Shows the N slowest test cases (N=0 for all)

With these command-line options, you can fine-tune how your tests run. In day-to-day test running, the -v option is probably the most commonly used. This option works the same as setting the verbosity argument to 2 when calling the unittest.main() function.

Grouping Your Tests With the TestSuite Class

The unittest framework has a class called TestSuite that you can use to create groups of tests and run them selectively. Test suites can be useful in many situations, including the following:

  • Complex projects: In complex projects with many features, test suites help you organize tests into manageable and logical groups.
  • Different testing levels: Test suites allow you to organize your tests according to their testing levels, including unit tests, integration tests, and system tests.
  • Selective testing: Test suites allow you to create logical groups of tests that you can run selectively, saving time and resources.
  • Environment-specific testing: Test suites allow group tests that are supposed to run on specific platforms, such as Windows, Linux, macOS, or others.

To illustrate how to create test suites, say that you have a module called calculations.py that defines basic arithmetic and statistical operations:

Python calculations.py
import math
from collections import Counter

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

def subtract(x, y):
    return x - y

def multiply(x, y):
    return x * y

def divide(x, y):
    if y == 0:
        raise ZeroDivisionError("Cannot divide by zero.")
    return x / y

def mean(data):
    return sum(data) / len(data)

def median(data):
    n = len(data)
    index = n // 2
    if n % 2:
        return sorted(data)[index]
    return sum(sorted(data)[index - 1 : index + 1]) / 2

def mode(data):
    c = Counter(data)
    return [k for k, v in c.items() if v == c.most_common(1)[0][1]]

In this module, you have several functions. Some of them are basic arithmetic operations, and others are statistical operations. From a testing point of view, a good approach is to write the following test cases:

Python test_calculations.py
import unittest

from calculations import (
    add,
    divide,
    mean,
    median,
    mode,
    multiply,
    subtract,
)

class TestArithmeticOperations(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(10, 5), 15)
        self.assertEqual(add(-1, 1), 0)

    def test_subtract(self):
        self.assertEqual(subtract(10, 5), 5)
        self.assertEqual(subtract(-1, 1), -2)

    def test_multiply(self):
        self.assertEqual(multiply(10, 5), 50)
        self.assertEqual(multiply(-1, 1), -1)

    def test_divide(self):
        self.assertEqual(divide(10, 5), 2)
        self.assertEqual(divide(-1, 1), -1)
        with self.assertRaises(ZeroDivisionError):
            divide(10, 0)

class TestStatisticalOperations(unittest.TestCase):
    def test_mean(self):
        self.assertEqual(mean([1, 2, 3, 4, 5, 6]), 3.5)

    def test_median_odd(self):
        self.assertEqual(median([1, 3, 3, 6, 7, 8, 9]), 6)

    def test_median_even(self):
        self.assertEqual(median([1, 2, 3, 4, 5, 6, 8, 9]), 4.5)

    def test_median_unsorted(self):
        self.assertEqual(median([7, 1, 3, 3, 2, 6]), 3)

    def test_mode_single(self):
        self.assertEqual(mode([1, 2, 2, 3, 4, 4, 4, 5]), [4])

    def test_mode_multiple(self):
        self.assertEqual(set(mode([1, 1, 2, 3, 4, 4, 5, 5])), {1, 4, 5})

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

These tests work as expected. Suppose you need a way to run the arithmetic and statistical tests separately. In this case, you can create test suites. In the following sections, you’ll learn how to do that.

Creating Test Suites With the TestSuite() Constructor

The TestSuite class allows you to create test suites. The class constructor takes the tests argument that must be an iterable of tests or other test suites. So, you can create your test suites like in the following code snippet:

Python test_calculations.py
# ...

def make_suite():
    arithmetic_tests = [
        TestArithmeticOperations("test_add"),
        TestArithmeticOperations("test_subtract"),
        TestArithmeticOperations("test_multiply"),
        TestArithmeticOperations("test_divide"),
    ]
    return unittest.TestSuite(tests=arithmetic_tests)

if __name__ == "__main__":
    suite = make_suite()
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

In the make_suite() function, you create a list of all tests from the TestArithmeticOperations test case. Then, you create and return a test suite using the TestSuite() constructor with the list of tests as an argument.

To run the suite, you create a TextTestRunner and pass the suite to its .run() method. If you run this file, then you get the following output:

Shell
$ python test_calculations.py
test_add (__main__.TestArithmeticOperations.test_add) ... ok
test_subtract (__main__.TestArithmeticOperations.test_subtract) ... ok
test_multiply (__main__.TestArithmeticOperations.test_multiply) ... ok
test_divide (__main__.TestArithmeticOperations.test_divide) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

This command only runs the four tests in the suite, skipping the rest of the tests in the test_calculations.py file.

Adding Tests to a Suite: .addTest() and .addTests()

You can also use the .addTest() method to add individual tests to an existing suite. To do this, you can do something like the following:

Python
# ...

def make_suite():
    arithmetic_suite = unittest.TestSuite()
    arithmetic_suite.addTest(TestArithmeticOperations("test_add"))
    arithmetic_suite.addTest(TestArithmeticOperations("test_subtract"))
    arithmetic_suite.addTest(TestArithmeticOperations("test_multiply"))
    arithmetic_suite.addTest(TestArithmeticOperations("test_divide"))
    return arithmetic_suite

if __name__ == "__main__":
    suite = make_suite()
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

This new version of make_suite() works like the previous section. Instead of using the class constructor to build the test suite, you use the .addTest() method. This approach can be useful when you have an existing test suite, and you need to add more tests to it.

The TestSuite class also has a .addTests() method that you can use to add several tests in one go. This method takes an iterable of test cases, test suites, or a combination of them. Consider the following example that creates a test suite with the statistical tests:

Python test_calculations.py
# ...

def make_suite():
    statistical_tests = [
        TestStatisticalOperations("test_mean"),
        TestStatisticalOperations("test_median_odd"),
        TestStatisticalOperations("test_median_even"),
        TestStatisticalOperations("test_median_unsorted"),
        TestStatisticalOperations("test_mode_single"),
        TestStatisticalOperations("test_mode_multiple"),
    ]
    statistical_suite = unittest.TestSuite()
    statistical_suite.addTests(statistical_tests)

    return statistical_suite

if __name__ == "__main__":
    suite = make_suite()
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

In this example, you create a list of tests from the TestStatisticalOperations class. Then, you create a TestSuite() instance and add the list of tests using .addTests(). Finally, you return the test suite as usual.

Go ahead and run the file from your command line:

Shell
$ python test_calculations.py
test_mean (__main__.TestStatisticalOperations.test_mean) ... ok
test_median_odd (__main__.TestStatisticalOperations.test_median_odd) ... ok
test_median_even (__main__.TestStatisticalOperations.test_median_even) ... ok
test_median_unsorted (__main__.TestStatisticalOperations.test_median_unsorted) ... ok
test_mode_single (__main__.TestStatisticalOperations.test_mode_single) ... ok
test_mode_multiple (__main__.TestStatisticalOperations.test_mode_multiple) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.000s

OK

This time, the command only runs the statistical tests. As you can see, test suites are a great way to run tests selectively.

Creating Suites With the load_tests() Function

Adding tests to a suite manually can be a tedious task. It can also be error-prone and represent a maintenance burden. Fortunately, unittest has other tools that can help you create test suites quickly.

The load_tests() function is one of these tools. The function is a hook that unittest provides for customizing test loading and suite creation, either for modules or packages of tests.

The function takes three mandatory arguments. Here’s the signature:

Python
def load_tests(loader, standard_tests, pattern):

The loader argument will hold a test loader, which normally is an instance of TestLoader. When you define load_tests() in a module, the standard_tests argument will receive the tests that are loaded from the module by default. When you define the function in a package, the standard_tests will get the test loaded from the package’s __init__.py file. Finally, the pattern argument is a glob pattern that you can use when discovering the tests.

When you call unittest.main() in a test module, the load_tests() function defined in the module gets called automatically, and unittest takes care of passing in the required arguments. This behavior allows you to build a test suite with minimal boilerplate code.

Here’s an example of how to create two test suites. One for the arithmetic tests and another for the statistical tests:

Python test_calculations.py
# ...

def load_tests(loader, standard_tests, pattern):
    suite = unittest.TestSuite()
    suite.addTests(loader.loadTestsFromTestCase(TestArithmeticOperations))
    suite.addTests(loader.loadTestsFromTestCase(TestStatisticalOperations))
    return suite

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

After you create the test suite, you use the .addTests() method to add multiple tests in one go. To build the list of tests, you call the .loadTestsFromTestCase() on the loader argument, which is the default test loader.

Note that in this example, you don’t use the standard_tests or the pattern argument. The former is useful in test packages where you’d like to add tests to those found in __init__.py and build a test suite from there. The pattern argument is also for loading tests from packages rather than from modules.

Creating Test Fixtures

A test fixture is a preparation that you perform before and after running one or more tests. The preparations before the test run are known as setup, while the tasks that you perform after the test run are called teardown.

The setup process may involve the creation of temporary files, objects, databases, dataframes, network connections, and so on. In contrast, the teardown phase may require releasing resources, removing temporary files, closing connections, and similar tasks.

The unittest framework allows you to create setup and teardown fixtures in your test cases classes by overriding the following methods in your TestClass subclasses:

Method Description
.setUp() An instance method that unittest calls before running each test method in a test case class.
.tearDown() An instance method that unittest calls after running each test method in a test case class.
.setUpClass() A class method that unittest calls before running the tests in a test case class.
.tearDownClass() A class method that unittest calls after running the tests in a test case class.

The last two methods are class methods, which means that you need to use the @classmethod decorator to create them. Here’s how they should look:

Python
import unittest

class Test(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        ...

    @classmethod
    def tearDownClass(cls):
        ...

These methods only take the current test class as an argument. Remember that they run only once per class.

To create module-level fixtures, you need to use module-level functions rather than methods on a TestCase subclass. The required functions are the following:

Function Description
setUpModule() Runs before all test cases in the containing module
tearDownModule() Runs after all test cases have run

If an exception occurs in the setUpModule() function, then none of the tests in the module run, and the tearDownModule() function won’t run either.

In the following sections, you’ll learn how to create fixtures using the capabilities of unittest.

Test Fixtures

As an example of how to use fixtures, say that you have the following implementation of a stack data structure:

Python stack.py
class Stack:
    def __init__(self, items=None):
        self.items = list(items) if items is not None else []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def __len__(self):
        return len(self.items)

    def __iter__(self):
        return iter(self.items)

    def __reversed__(self):
        return reversed(self.items)

In this module, you define the Stack class that provides the .push() and .pop() methods. The former method appends a new item to the top of the stack, while the latter method removes and returns the item at the top of the stack.

Then, you implement three special methods. The .__len__() method supports the built-in len() function. The .__iter__() method allows you to use Stack instances in for loops. Finally, the .__reversed__() method allows you to support the built-in reversed() function.

Now, say that you need to write a TestCase subclass to test this class. The test class will have to test all the features of your Stack class. In this situation, you have at least two options:

  1. You can create a new an independent instance of Stack for every test.
  2. You can create a single instance of the class for all the tests.

The first approach sounds tedious and may require a lot of repetitive code. The second approach sounds better, so you decide to go with it. In this situation, you can use a class-level setup fixture to create the instance of Stack and reuse it in every test:

Python test_stack.py
import unittest

from stack import Stack

class TestStack(unittest.TestCase):
    def setUp(self):
        self.stack = Stack()

    def tearDown(self):
        del self.stack

    def test_push(self):
        self.stack.push(1)
        self.assertEqual(self.stack.items, [1])

    def test_pop(self):
        self.stack.push(2)
        item = self.stack.pop()
        self.assertEqual(item, 2)

    def test_len(self):
        self.stack.push(3)
        self.stack.push(4)
        self.assertEqual(len(self.stack), 2)

    def test_iter(self):
        items = [5, 6, 7]
        for item in items:
            self.stack.push(item)
        for stack_item, test_item in zip(self.stack, items):
            self.assertEqual(stack_item, test_item)

    def test_reversed(self):
        items = [5, 6, 7]
        for item in items:
            self.stack.push(item)
        reversed_stack = reversed(self.stack)
        self.assertEqual(list(reversed_stack), [7, 6, 5])

if __name__ == "__main__":
    unittest.main(verbosity=2)

In this test module, you create the TestStack to test the different features of your Stack class. The first two methods define the setup and teardown logic, which consist of creating and removing an instance of Stack, respectively.

When you run the tests, unittest automatically calls .setUp() and .tearDown() before and after running each test method. This way, every test method has a fresh instance to work on. In other words, these two methods save you from creating a fresh instance of Stack in each test method.

Class-Level Fixtures

If you use the .setUpClass() and .tearDownClass() class methods, then you can create class-level fixtures. This type of fixture only runs once per test case class. The .setUpClass() method runs before the test methods, and .tearDownClass() runs after all the test methods have run.

This behavior is known as shared fixtures because all the test methods depend on a single setup and teardown run. Note that shared fixtures break test isolation. In other words, the results of a test will depend on previously run tests. So, they should be used with care.

To illustrate how class-level fixtures work, say you have the following Employee class:

Python employee.py
class Employee:
    __slots__ = ["name", "age", "job", "salary"]

    def __init__(self, name, age, job, salary):
        self.name = name
        self.age = age
        self.job = job
        self.salary = salary

    def profile(self):
        for attr in self.__slots__:
            print(f"{attr.capitalize()}: {getattr(self, attr)}")
        print()

The class represents an employee of your company. The class has four attributes to store information about each employee. Because you’re planning to create a large number of instances of this class, you use the .__slots__ attribute to reduce the memory footprint.

The employees’ data lives in a CSV file that looks something like the following:

CSV employees.csv
name,age,job,salary
Alice,25,Engineer,50000
Bob,30,Analyst,60000
Jane,35,Manager,80000
John,40,CEO,100000
...

You need to define a function that reads this file and returns a list of employees. This function can look something like the following:

Python employee.py
import csv

class Employee:
    # ...

def from_csv_file(file_path):
    with open(file_path) as file:
        reader = csv.DictReader(file)
        employees = []
        for row in reader:
            employees.append(
                Employee(
                    name=row["name"],
                    age=int(row["age"]),
                    job=row["job"],
                    salary=float(row["salary"]),
                )
            )
        return employees

In the from_csv_file() function, you read a CSV file using the DictReader from the csv module. Then, you run a loop to create a list of Employee instances with the read data. Now, you want to write tests for this function:

Python test_employee.py
import os
import unittest
from tempfile import NamedTemporaryFile

from employee import from_csv_file

SAMPLE_CSV = """name,age,job,salary
Alice,25,Engineer,50000
Bob,30,Analyst,60000
Jane,35,Manager,80000
John,40,CEO,100000
"""

class TestFromCsvFile(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.temp_file = NamedTemporaryFile(
            delete=False,
            mode="w",
            newline="",
            suffix=".csv",
        )
        cls.temp_file_name = cls.temp_file.name
        cls.temp_file.write(SAMPLE_CSV)
        cls.temp_file.close()
        cls.employees = from_csv_file(cls.temp_file_name)

    @classmethod
    def tearDownClass(cls):
        os.remove(cls.temp_file_name)

    def test_from_csv_file_total_employees(self):
        self.assertEqual(len(self.employees), 4)

    def test_from_csv_file_employee_attributes(self):
        self.assertEqual(self.employees[0].name, "Alice")
        self.assertEqual(self.employees[0].age, 25)
        self.assertEqual(self.employees[0].job, "Engineer")
        self.assertEqual(self.employees[0].salary, 50000.0)

    def test_from_csv_file_employee_name(self):
        self.assertEqual(self.employees[0].name, "Alice")
        self.assertEqual(self.employees[1].name, "Bob")
        self.assertEqual(self.employees[2].name, "Jane")
        self.assertEqual(self.employees[3].name, "John")

if __name__ == "__main__":
    unittest.main(verbosity=2)

In this TestFromCsvFile, you have the .setUpClass() method. In this method, you create a new temporary file using NamedTemporaryFile from the tempfile module.

To populate this file, you use the sample data stored in the SAMPLE_CSV constant. After wiring the sample data to the temporary file, you close the file and call from_csv_file() to create the list of employees. With these actions, you’ve set up everything to test the function. Note that all these actions run only once before all the test methods run.

In the tearDownClass(), you remove the temporary file to clean up your file system and free the acquired resources. This method will run after the test methods have run. Finally, you have three test methods that check different aspects of the generated list of employees.

Module-Level Fixtures

You can also create module-level fixtures. To make these types of fixtures, you need to define the following functions at the module level:

Python
def setUpModule():
    ...

def tearDownModule():
    ...

These fixtures run once per module. The setup fixture runs before all the test cases in the module, and the teardown fixture runs after all the test cases in the module have run.

If an exception happens in the setUpModule() function, then none of the tests in the module will run, and the tearDownModule() won’t run either. If the raised exception is a SkipTest exception, then the module will be reported as skipped instead of an error.

Module-level fixtures are useful when you have several TestCase subclasses in a module, and some of them will benefit from a common setup and teardown logic.

The classic example is a test module with a few test cases that check for database-related functionalities. These tests may need an active connection to the database, which you can create in the setUpModule() function and close in the tearDownModule() function.

Debugging Failing Tests

Up to this point, you haven’t dealt with failing tests in unittest. However, failing tests are probably the most important part of any code testing process. Failing tests allow you to fix your code and make it work as expected. The unittest framework provides descriptive outputs for failing tests. These outputs can help you debug your code and fix it.

A Quick Example: FizzBuzz

Consider the following function that tries to solve the FizzBuzz challenge, where you return "fizz" for numbers divisible by 3, "buzz" for those divisible by 5, and "fizz buzz" for those divisible by both 3 and 5.

Go ahead and create a fizzbuzz.py file and add the following code to it:

Python fizzbuzz.py
def fizzbuzz(number):
    if number % 3 == 0:
        return "fizz"
    elif number % 5 == 0:
        return "buzz"
    elif number % 15 == 0:
        return "fizz buzz"
    else:
        return number

This function works okay for numbers divisible by 3 or 5. However, there is a slight issue with numbers that are divisible by both.

Here’s a unittest test case for your fizzbuzz() function:

Python test_fizzbuzz.py
import unittest

from fizzbuzz import fizzbuzz

class TestFizzBuzz(unittest.TestCase):
    def test_fizz(self):
        self.assertEqual(fizzbuzz(3), "fizz")

    def test_buzz(self):
        self.assertEqual(fizzbuzz(5), "buzz")

    def test_fizz_buzz(self):
        self.assertEqual(fizzbuzz(15), "fizz buzz")

    def test_neither(self):
        self.assertEqual(fizzbuzz(1), 1)
        self.assertEqual(fizzbuzz(2), 2)
        self.assertEqual(fizzbuzz(4), 4)

if __name__ == "__main__":
    unittest.main(verbosity=2)

The first two tests will pass. However, the third test won’t pass because the function has a bug. Go ahead and run the tests so that you can check the output of a failing test:

Shell
$ python test_fizzbuzz.py
test_buzz (__main__.TestFizzBuzz.test_buzz) ... ok
test_fizz (__main__.TestFizzBuzz.test_fizz) ... ok
test_fizz_buzz (__main__.TestFizzBuzz.test_fizz_buzz) ... FAIL
test_neither (__main__.TestFizzBuzz.test_neither) ... ok

======================================================================
FAIL: test_fizz_buzz (__main__.TestFizzBuzz.test_fizz_buzz)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../test_fizzbuzz.py", line 13, in test_fizz_buzz
    self.assertEqual(fizzbuzz(15), "fizz buzz")
AssertionError: 'fizz' != 'fizz buzz'
- fizz
+ fizz buzz

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=1)

In this output, the first highlighted line points you to the failing test. The second highlighted line lets you know the exact line where you can find the failing assertion. Finally, the third highlighted line tells you that the actual result and the expected result are unequal.

The failing test tells you that the fizzbuzz() function has an issue with numbers that are divisible by both 3 and 5. So, you need to modify the code to make the test pass:

Python fizzbuzz.py
def fizzbuzz(number):
    if number % 15 == 0:
        return "fizz buzz"
    elif number % 3 == 0:
        return "fizz"
    elif number % 5 == 0:
        return "buzz"
    else:
        return number

In this update, you moved the condition that checks for numbers divisible by 3 and 5 to the beginning of the function. Now you can run the tests again:

Shell
$ python test_fizzbuzz.py
test_buzz (__main__.TestFizzBuzz.test_buzz) ... ok
test_fizz (__main__.TestFizzBuzz.test_fizz) ... ok
test_fizz_buzz (__main__.TestFizzBuzz.test_fizz_buzz) ... ok
test_neither (__main__.TestFizzBuzz.test_neither) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

Your fizzbuzz() function works correctly after the update. This quick example uncovers the essence of code testing, where you write tests to ensure that your code works as expected so you can fix any issue that the tests reveal.

A Test-Driven Example: Rock, Paper, and Scissors

A well-known philosophy around code testing is the test-driven development (TDD) methodology. In TDD, you convert the code’s requirements into test cases before you write the actual code.

For example, say that you want to code a function that takes a number from 0 to 2 and return the strings "rock", "paper", or "scissors". Instead of writing the function right away, you start writing the following tests:

Python test_game.py
import unittest

from game import rock_paper_scissors

class TestRockPaperScissors(unittest.TestCase):
    def test_out_of_range(self):
        with self.assertRaises(ValueError):
            rock_paper_scissors(42)

    def test_rock(self):
        self.assertEqual(rock_paper_scissors(0), "rock")

    def test_paper(self):
        self.assertEqual(rock_paper_scissors(1), "paper")

    def test_scissors(self):
        self.assertEqual(rock_paper_scissors(2), "scissors")

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

The first test provides a way to check whether the function validates the input argument, which must be a number between 0 and 2. The other tests check the three possible inputs and the expected output for each of them.

Now you can write your function to gradually pass the tests:

Python game.py
def rock_paper_scissors(choice):
    if choice < 0 or choice > 2:
        raise ValueError("choice must be 0, 1, or 2")

In this snippet, you wrote the code to pass the first test. Go ahead and run the tests from your command line:

Shell
$ python test_game.py
.FFF
======================================================================
FAIL: test_paper (__main__.TestRockPaperScissors.test_paper)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../test_game.py", line 15, in test_paper
    self.assertEqual(rock_paper_scissors(1), "paper")
AssertionError: None != 'paper'

======================================================================
FAIL: test_rock (__main__.TestRockPaperScissors.test_rock)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../test_game.py", line 12, in test_rock
    self.assertEqual(rock_paper_scissors(0), "rock")
AssertionError: None != 'rock'

======================================================================
FAIL: test_scissors (__main__.TestRockPaperScissors.test_scissors)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../test_game.py", line 18, in test_scissors
    self.assertEqual(rock_paper_scissors(2), "scissors")
AssertionError: None != 'scissors'

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=3)

In this output, the first line contains the .FFF text. The dot means that you have a passing test, which is the test_out_of_range() test. The Fs mean that you have three failing tests, which are those that deal with the function’s primary goal.

Next, you can write the required code to pass the rest of the tests:

Python game.py
def rock_paper_scissors(choice):
    if choice < 0 or choice > 2:
        raise ValueError("choice must be 0, 1, or 2")

    if choice == 0:
        return "rock"

In this snippet, you add the required code to pass the test_rock() test. Now you can run the tests again:

Shell
$ python test_game.py
.F.F
======================================================================
FAIL: test_paper (__main__.TestRockPaperScissors.test_paper)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../test_game.py", line 15, in test_paper
    self.assertEqual(rock_paper_scissors(1), "paper")
AssertionError: None != 'paper'

======================================================================
FAIL: test_scissors (__main__.TestRockPaperScissors.test_scissors)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ".../test_game.py", line 18, in test_scissors
    self.assertEqual(rock_paper_scissors(2), "scissors")
AssertionError: None != 'scissors'

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=2)

This time you have two failing tests, the test_paper() and test_scissors() tests. During the development, you realize that there’s a better way to code the function:

Python game.py
def rock_paper_scissors(choice):
    if choice < 0 or choice > 2:
        raise ValueError("choice must be 0, 1, or 2")

    choices = ["rock", "paper", "scissors"]
    return choices[choice]

In this final implementation of rock_paper_scissors(), you have a list containing the possible return values. Then, you use the input argument to index the list and return the appropriate string.

Go ahead and run your tests again:

Shell
$ python test_game.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK

Great! Your function passes all the tests. To code this function, you’ve used an incremental development process driven by test cases. At the end of the process, your code works as expected. So, it’s ready to use!

Testing With Fake Objects: unittest.mock

The unittest.mock module provides two base classes that allow you to mock, or simulate, external resources, such as files, connections, and so on:

  1. Mock is a general generic mock object.
  2. MagicMock is the same as Mock, but it includes magic methods.

As a quick example of how to mock an object, consider the following sample module:

Python weekday.py
import datetime

def is_weekday():
    today = datetime.date.today()
    return 0 <= today.weekday() < 5

This module defines the is_weekday() function, which returns True if the current day is a weekday and False otherwise. There’s no easy way to test this function because time never stops. If you write the test today, then that test can fail in a few days because the result of calling datetime.date.today() changes every day.

To pass this challenge and make your tests work, you can mock the .today() method to return a fixed date. Here is a test module that does that:

Python test_weekday.py
import datetime
import unittest
from unittest.mock import patch

import weekday

class TestWeekday(unittest.TestCase):
    @patch("weekday.datetime")
    def test_is_weekday(self, mock_datetime):
        mock_datetime.date.today.return_value = datetime.date(2024, 4, 4)
        self.assertTrue(weekday.is_weekday())

    @patch("weekday.datetime")
    def test_is_weekend(self, mock_datetime):
        mock_datetime.date.today.return_value = datetime.date(2024, 4, 6)
        self.assertFalse(weekday.is_weekday())

if __name__ == "__main__":
    unittest.main(verbosity=2)

In this module, you write a test case class with two test methods. You use the @patch decorator to create a Mock object around the datetime module. Note that this is patching the datetime module that you imported there in your weekday module. This Mock object goes to the mock_datetime argument of the underlying test method.

Then, you create fake return values for .today() in each test method. The fake return value in .test_is_weekday() is a Thursday, so is_weekday() must return True and that’s what you check. Similarly, the fake return value in .test_is_weekend() is a Saturday, and is_weekday() must return False.

That’s it! Now, your tests will always work, regardless of the date, because you’ve patched the default behavior of .today() to make it return a fixed date.

Conclusion

Now you know how to write unit tests for your Python code using the unittest testing framework. This framework comes with the Python standard library, so you don’t have to install third-party packages to start writing your tests.

The unittest framework supports several useful features that you’ve also learn how to use. These features include test cases, fixtures, test suites, test discovery, and more.

In this tutorial, you’ve learned how to:

  • Create unittest tests with the TestCase class
  • Explore the assert methods on the TestCase class
  • Run your tests using the command-line interface of unittest
  • Group test cases using the TestSuite class
  • Handle setup and teardown logic using fixtures

With this knowledge, you’re ready to start writing robust unit tests for your Python code without needing to install additional packages.

Take the Quiz: Test your knowledge with our interactive “Python's unittest: Writing Unit Tests for Your Code” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Python's unittest: Writing Unit Tests for Your Code

In this quiz, you'll test your understanding of Python testing with the unittest framework from the standard library. With this knowledge, you'll be able to create basic tests, execute them, and find bugs before your users do.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Leodanis Pozo Ramos

Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 6+ years of experience. He's an avid technical writer with a growing number of articles published on Real Python and other sites.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!