Python comes with the built-in unittest module, which is a framework to write automated tests based on JUnit, a unit testing framework for Java. You create tests by subclassing from unittest.TestCase and defining methods that begin with test. Here's an example of a typical minimal test case using unittest:
import unittest
from fibo import fibonacci
class Test(unittest.TestCase):
def test_fibo(self):
result = fibonacci(4)
self.assertEqual(result, 3)
if __name__ == '__main__':
unittest.main()
The focus of this example is on showcasing the test itself, not the code being tested, so we will be using a simple fibonacci function. The Fibonacci sequence is an infinite sequence of positive integers where the next number in the sequence is found by summing up the two previous numbers. Here are the first 11 numbers:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
Our fibonacci function receives an index of the fibonacci sequence, computes the value on the fly, and returns it.
To ensure the function is working as expected, we call it with a value that we know the correct answer for (the fourth element of the Fibonacci series is 3), then the self.assertEqual(a, b) method is called to check that a and b are equal. If the function has a bug and does not return the expected result, the framework will tell us when we execute it:
λ python3 -m venv .env
source .env/bin/activate
F
======================================================================
FAIL: test_fibo (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_fibo.py", line 8, in test_fibo
self.assertEqual(result, 3)
AssertionError: 5 != 3
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
It seems there's a bug in our fibonacci function and whoever wrote it forgot that for n=0 it should return 0. Fixing the function and running the test again shows that the function is now correct:
λ python test_fibo.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
This is great and is certainly a step in the right direction. But notice that, in order to code this very simple check, we had to do a number of things not really related to the check itself:
- Import unittest
- Create a class subclassing from unittest.TestCase
- Use self.assertEqual() to do the checking; there are a lot of self.assert* methods that should be used for all situations like self.assertGreaterEqual (for ≥ comparisons), self.assertLess (for < comparisons), self.assertAlmostEqual (for floating point comparisons), self.assertMultiLineEqual() (for multi-line string comparisons), and so on
The above feels like unnecessary boilerplate, and while it is certainly not the end of the world, some people feel that the code is non-Pythonic; code is written just to placate the framework into doing what you need it to.
Also, the unittest framework doesn't provide much in terms of batteries included to help you write your tests for the real world. Need a temporary directory? You need to create it yourself and clean up afterwards. Need to connect to a PostgreSQL database to test a Flask application? You will need to write the supporting code to connect to the database, create the required tables, and clean up when the tests end. Need to share utility test functions and resources between tests? You will need to create base classes and reuse them through subclassing, which in large code bases might evolve into multiple inheritance. Some frameworks provide their own unittest support code (for example, Django, https://www.djangoproject.com/), but those frameworks are rare.