The unittest module has been a part of the Python standard library since Python 2.1. As its name would suggest, it helps developers write and run tests for their code.
Unit testing involves checking code for bugs by testing the smallest testable pieces of code. These tiny chunks of code are termed “units.” Testing code this way helps verify that every part of a program – including the helper functions that a user might not be aware of – does what it is intended to do.
Besides helping find bugs, unit tests also help prevent regressions in code as it is altered over time.
For these reasons, TDD-driven development teams find the module essential, and these teams ensure that all the code has some tests for it.
Bear in mind that unit testing is different from integration and regression testing, where tests are designed to check whether different parts of a program work together and produce the intended result.
Put simply, writing unit tests is an excellent way to fail-proof code.
In this guide, we will walk you through using the unittest module to write tests. You will need a basic understanding of Python functions to learn from this guide effectively.
The Main Idea of Unit Testing
Think back to high school:
Math problems involved using a varied set of arithmetic procedures to come up with different solutions. These solutions were then combined to get the right final answer.
Many of us would read through the solutions again to ensure we did every step correctly. And more importantly, to check that we didn’t miswrite anything or make mistakes and that the calculations at the end of every step were correct.
This checking of our steps and calculations captures what we do with unit testing. Making mistakes during coding is a part of writing good code – just like it’s a part of getting the right answer in math.
But going back to old code and checking it frantically to verify its correctness is not the right approach. It can be downright frustrating.
Let’s say you write a small and simple piece of code to determine the area of a rectangle. To check whether the code works correctly, you could pass various numbers and see whether the calculations are right.
A unit test would carry forward this same checking process for you.
Unit tests are a standard and critical component of regression testing and help ensure that code performs as expected when changes are made. Plus, the tests help ensure the stability of a program.
So, when changes are made to a program, you can run the corresponding unit tests written beforehand to check that its existing functionality does not impact other parts of the codebase.
One of the key benefits of unit testing is that it helps isolate errors. If you were to run an entire project’s code and receive several errors, debugging the code would be very tedious.
The outputs of unit tests make it simple to determine whether any part of the code is throwing errors. The debugging process can start from there.
This is not to say that unit tests always help find the bug. But one thing’s for sure; these tests provide a convenient starting point for bug hunting before you check the integration components in integration testing.
Now that you fully understand the motivation behind using unit tests, let's explore how you can implement them and make them a part of your development pipeline.
Basics of Unit Testing in Python
Also referred to as PyUnit, the unit test module is based on the XUnit framework design created by Erich Gamma and Kent Beck.
You will find similar unit testing modules in several other programming languages, including Java and C.
As you might know, unit testing can be done both manually and automatically with tool support. Automated testing is faster, more reliable, and reduces the human resource cost of testing, which is why most development teams prefer unit testing.
The unittest module’s framework supports test suites, fixtures, and a test runner, enabling automated testing.
Structure of a Unit Test
In unit testing, a test has two parts. One part involves the code to manage the test’s “fixtures,” and the second is the test itself.
A test is written by subclassing TestCase and either adding an appropriate method or overriding. Here’s an example:
import unittest class SimpleTest(unittest.TestCase): def test(self): self.failUnless(True) if __name__ == '__main__': unittest.main()
The SimpleTest class above has one test() method that fails if True becomes False.
Running a Unit Test
The straightforward method of running a unit test is to write the following at the bottom of a test file:
if __name__ == '__main__': unittest.main()
You can then run the script from the command line. You can expect an output that looks like this:
. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK |
The single dot at the beginning of the output indicates that the test has passed.
The output of a unit test will always include the number of tests run. It will also include a status indicator for every test. But if you want more details about the test, you can use the -v option.
Outcomes of a Unit Test
The output of a unit test can indicate one of three things:
- OK – this means your test has passed.
- FAIL – this means there’s an AssertionError exception, and the test didn’t pass.
- ERROR – this means the test raised an exception other than AssertionError.
You’re now familiar with the basics of unit tests in Python. We’ll now learn how to implement unit tests with examples.
Defining TestCase
The TestCase class is one of the most critical classes included in unittest. The class provides the basic code structure with which you can test functions in Python.
Here’s an example:
import unittest def put_fish_in_aquarium(list_of_fishes): if len(list_of_fishes) > 10: raise ValueError("The aquarium cannot hold more than ten fish") return {"tank_1": list_of_fishes} class TestPutFishInAquarium(unittest.TestCase): def test_put_fish_in_aquarium_success(self): actual = put_fish_in_aquarium(list_of_fishes=["guppy", "goldfish"]) expected = {"tank_1": ["guppy", "goldfish"]} self.assertEqual(actual, expected)
Here’s a breakdown of what’s happening in this code:
The unittest module is imported to make it available to the code. Next, the function that needs testing is defined. In this case, the function is called put_fish_in_aquarium.
The function is written to accept a list of fishes, and if the list has more than ten entries, it raises an error.
Next, the function returns a dictionary mapping of the fish tank, defined in the code as “tank_1,” to the list of fishes.
The TestPutFishInAquarium class is defined as a subclass of unittest.TestCase. The test_put_fish_in_aquarium_success method is defined in the class. The method calls the put_fish_in_aquarium function with a certain input and then verifies that the returned value is the same as the expected return value.
Let’s move on to exploring the steps of executing this test.
Executing a TestCase
Let’s assume the code in the previous section is saved as “test_fish_in_aquarium.py.” To execute it, navigate to the directory with the Python file in it in your command line, and run the command:
python -m unittest test_put_fish_in_aquarium.py
In the above line, we’ve invoked the unittest Python module with the “python -m unittest” part of the line. The next characters in the line define the path to the file with the previously written TestCase as an argument.
When you run the command, the command line will supply the following output:
Output . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK |
As you can tell, the test has passed.
Let’s look at a test with a failure.
We’ve changed the values of the “expected” variable in the code below to make the test fail:
import unittest def put_fish_in_aquarium(list_of_fishes): if len(list_of_fishes) > 10: raise ValueError("The aquarium cannot hold more than ten fish") return {"tank_1": list_of_fishes} class TestPutFishInAquarium(unittest.TestCase): def test_put_fish_in_aquarium_success(self): actual = put_fish_in_aquarium(list_of_fishes=["guppy", "goldfish"]) expected = {"tank_1": ["tiger"]} self.assertEqual(actual, expected)
To run the code, you can run the same command line entry we described earlier. The output will look as follows:
Output F ====================================================================== FAIL: test_put_fish_in_aquarium_success (test_put_fish_in_aquarium.TestPutFishInAquarium) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_put_fish_in_aquarium.py", line 13, in test_put_fish_in_aquarium_success self.assertEqual(actual, expected) AssertionError: {'tank_1': ['guppy', 'goldfish']} != {'tank_1': ['tiger']} - {'tank_1': ['guppy', 'goldfish']} + {'tank_1': ['tiger']} ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1) |
Notice how the beginning of the output now has an “F” instead of a “.” It indicates a failed test.
Testing a Function with an Exception
One of the nice things about the unittest module is that you can also use it to verify whether a test raises a ValueError exception. The exception will be raised when there are too many entries in the list.
Let’s extend the same example we’ve discussed by adding a new test method to it:
import unittest def put_fish_in_aquarium(list_of_fishes): if len(list_of_fishes) > 10: raise ValueError("The aquarium cannot hold more than ten fish") return {"tank_1": list_of_fishes} class TestPutFishInAquarium(unittest.TestCase): def test_put_fish_in_aquarium_success(self): actual = put_fish_in_aquarium(list_of_fishes=["guppy", "goldfish"]) expected = {"tank_1": ["tiger"]} self.assertEqual(actual, expected) def test_put_fish_in_aquarium_exception(self): excess_fish = ["guppy"] * 25 with self.assertRaises(ValueError) as exception_context: put_fish_in_aquarium(list_of_fishes=excess_fish) self.assertEqual( str(exception_context.exception), " The aquarium cannot hold more than ten fish." )
The new test method defined in the code invokes the put_fish_in_aquarium method, just like the first method we defined. However, this method invokes the function with a list of 25 “guppy” strings.
The new method uses the with self.assertRaises(...) context manager, which is a default part of the TestCase method, to check whether the put_fish_in_aquarium method rejects this list of strings because it’s too long.
The exception class we expect to appear, ValueError, is the first argument to self.assertRaises. The context manager involved is bound to a certain variable called exception_context.
This variable has an exception attribute with the underlying ValueError that the put_fish_in_aquarium method raises. When the str() on the ValueError is called to retrieve the message it has, it returns the expected exception.
Running the test again, you will see the following output:
Output .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK |
So, the test passes. What’s important to remember is that unittest.TestCase supplies several methods beyond assertRaises and assertEqual. We’ve defined a few of the noteworthy ones below, but you can find the full list of supported assertion methods in the documentation.
Method |
Assertion |
assertEqual(a, b) |
a == b |
assertFalse(a) |
bool(a) is False |
assertIn(a, b) |
a in b |
assertIsNone(a) |
a is None |
assertIsNotNone(a) |
a is not None |
assertNotEqual(a, b) |
a != b |
assertNotIn(a, b) |
a not in b |
assertTrue(a) |
bool(a) is True |
So far, we’ve covered some basic unit tests you can introduce into your code. Let’s move on to exploring the other tools supplied by TestCase that you could use to test your code.
The setUp Method
One of the most useful methods that TestCase supports is the setUp method. It allows you to create resources for individual tests. Using this method is ideal when you have a common set of preparation code that you need to run before every test.
With setUp, you can put all of the preparation code in one place, removing the need to repeat it time and time again for every test. Here’s an example:
import unittest class FishBowl: def __init__(self): self.holding_water = False def put_water(self): self.holding_water = True class TestFishBowl(unittest.TestCase): def setUp(self): self.fish_bowl = FishBowl() def test_fish_bowl_empty_by_default(self): self.assertFalse(self.fish_bowl.holding_water) def test_fish_bowl_can_be_filled(self): self.fish_bowl.put_water() self.assertTrue(self.fish_bowl.holding_water)
The above code has a FishBowl class, and the holding_water instance is initially set to False. However, you can set it to True by calling the put_water() method.
The TestFishBowl method is defined in the TestCase subclass and defines setUp. This method instantiates a fresh instance of FishBowl, assigned to self.fish_bowl.
As established earlier, the setUp method runs before every individual method, and therefore, a new FishBowl instance is created for test_fish_bowl_empty_by_default and test_fish_bowl_can_be_filled.
The first of these two verifies that holding_water is False, and the second verifies that holding_water is True after put_water() is called.
Running this code, we will see the output:
Output .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK |
As you can see, both tests have passed.
---
Note: If there are several test files, running them all in one go will be more convenient. You can use “python -m unittest discover” in the command line to run more than one file.
---
The tearDown Method
The tearDown method is a counterpart of the setUp method, enabling you to remove connections to a database and reset the modifications made to a filesystem after a test. Here’s an example:
import os import unittest class AdvancedFishBowl: def __init__(self): self.fish_bowl_file_name = "fish_bowl.txt" default_contents = "guppy, goldfish" with open(self.fish_bowl_file_name, "w") as f: f.write(default_contents) def empty_tank(self): os.remove(self.fish_bowl_file_name) class TestAdvancedFishBowl(unittest.TestCase): def setUp(self): self.fish_bowl = AdvancedFishBowl() def tearDown(self): self.fish_bowl.empty_bowl() def test_fish_bowl_writes_file(self): with open(self.fish_bowl.fish_bowl_file_name) as f: contents = f.read() self.assertEqual(contents, "guppy, goldfish")
The AdvancedFishBowl class in the code above creates a file called fish_bowl.txt and then writes the string “guppy, goldfish” to it. The class also has an empty_bowl method that deletes the fish_bowl.txt file.
In the code, you will also find the TestAdvancedFishBowl TestCase subclass with both the setUp and tearDown methods.
You might be able to guess that the setUp method creates a fresh instance of AdvancedFishBowl and assigns it to self.fish_bowl.
On the other hand, the tearDown method calls the empty_bowl method on self.fish_bowl. In this way, it ensures that the fish_bowl.txt file is removed after every test ends. So, every test begins anew without the changes made by and the outcome of the previous test affecting it in any way.
The test_fish_bowl_writes_file method verifies that “guppy, goldfish” are written to fish_bowl.txt.
Running the code will give you the following output:
Output . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK |
How to Write Good Unit Tests?
There are a few tips to bear in mind that’ll help you write good unit tests:
#1 Carefully Name Your Tests
You need to note that in Python, TestCase recognizes test methods when they begin with the word “test.”
So, the following test will not work:
class TestAddition(unittest.TestCase): def add_test(self): self.assertEqual(functions.add(6, 1), 9)
You could write test_add, and that would work. But add_test will not work. This is important because if you write a method without “test” in front of it, your test will always pass. And this is because it did not run at all.
Having a test that you think passed is a lot worse than having no test at all. Making this mistake can completely mess up bug fixing.
Using a long name is the best way to go. The more specific you are with names, the easier it is to find bugs later.
#2 Make Simple Tests in the Beginning, Build Up Slowly
Writing unit tests that come to mind first is the right way to go since they’re bound to be important.
The idea is to write tests that check whether your functions are working correctly. If these tests pass, you can start writing more complicated tests. But never jump to writing complicated tests till you write tests checking the basic functionality of your code.
#3 Edge Cases
Writing tests for edge cases is an excellent way to approach unit testing. Let’s say you’re working with numbers in a program. What would happen if someone inputs negatives? Or a floating point number?
More curiously, does anything happen if the input is zero?
Zero has a reputation for breaking code, so if you’re working with numbers, it’s wise to have a test for it.
#4 Write Tests that are Independent of Each Other
Never write tests that depend on each other. It misses the point of unit testing, and there’s a good chance it’ll make bug testing tedious later.
For this reason, the unittest module has a built-in feature to prevent developers from doing this. It’s also important to note that the module doesn’t guarantee that tests will run in the order your specified.
However, you can use the setUp() and tearDown() methods to write code that is executed before and after every test.
#5 Steer Clear of Assert.IsTrue
Using assert.IsTrue is a bad idea because it doesn’t supply enough information. It only tells you whether the value is true or false. It’s much more helpful to use the assertEqual method since it’ll give you more details.
#6 Some of Your Test will Miss Things
Writing good tests takes practice, and even if you get good at it, there’s a chance that you will miss details.
The truth is that even the best developers cannot think of all the ways that a program will fail. However, writing good tests improves the chances of catching code-breaking bugs before they do their damage.
So, if you miss something in the code, don’t worry about it. Go back to the test file and write a new assertion for the case you’ve stumbled upon. This ensures that you don’t miss it in the future – even in the projects into which you copy the function and test file.
A useful test class written today is as useful as a good function written in the future.
Conclusion
Using the unittest module is one of the best approaches to getting started with testing since it involves writing short, maintainable methods to verify your code.
With the module, writing tests isn’t as time-consuming or hectic, seeing that tests are vital to all CI pipelines. The tests help catch bugs before things are so far down the pipeline that fixing a bug becomes a major time and cost expense.
The module has definitely made testing easier and more accessible. Developers don’t need to spend time learning or relying on external test frameworks. Several commands and libraries built into Python help you ensure that your applications work as intended.
However, as you garner more experience writing tests, you can consider switching to other, more powerful test frameworks like pytest. These frameworks tend to have advanced features that will help you flesh out your tests.