Overview
unittest is the standard Python unit testing framework. Inspired by JUnit, it is included with the standard CPython distribution. unittest provides a base class named TestCase, which provides methods for assertions and setup/cleanup routines. All test case classes must inherit from TestCase. Each method in a TestCase subclass whose name starts with “test” will be run as a test case. Tests can be grouped and loaded using the TestSuite class and load methods, which together can build custom test runners. unittest can also generate XML reports (like JUnit) using unittest-xml-reporting.
unittest is supported in both Python 2 and 3. However, use the unittest2 backport for versions earlier than Python 2.7.
Installation
Basic unittest does not need any special installation because it comes with Python. However, additional modules may be installed with pip if you need them:
> pip install unittest2 > pip install unittest-xml-reporting
Project Structure
Product code modules and unittest test code modules should be placed into separate Python packages within the same project. Test modules must be named “test_*.py” and must be put into packages in order for discovery to work when launching tests. Remember, a Python package is simply a directory with a file named “__init__.py“.
[project root directory] |‐‐ [product code packages] `‐‐ tests |‐‐ __init__.py `‐‐ test_*.py
Example Code
An example project named example-py-unittest is located in my GitHub python-testing-101 repository. The project has the following structure:
example-py-unittest |-- com.automationpanda.example | |-- __init__.py | `-- calc.py |-- com.automationpanda.tests | |-- __init__.py | `-- test_calc.py `-- README.md
The com.automationpanda.example.calc module contains a Calculator class with basic math methods:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Calculator(object): | |
def __init__(self): | |
self._last_answer = 0.0 | |
@property | |
def last_answer(self): | |
return self._last_answer | |
def add(self, a, b): | |
self._last_answer = a + b | |
return self.last_answer | |
def subtract(self, a, b): | |
self._last_answer = a – b | |
return self.last_answer | |
def multiply(self, a, b): | |
self._last_answer = a * b | |
return self.last_answer | |
def divide(self, a, b): | |
# automatically raises ZeroDivisionError | |
self._last_answer = a * 1.0 / b | |
return self.last_answer |
The com.automationpanda.tests.test_calc module contains a unittest.TestCase subclass, shown below. The test class uses the setUp method to construct a Calculator object, which each test method uses. The assertion methods used are assertEqual and assertRaises. A fresh instance of CalculatorTest is instantiated for every test method run.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from com.automationpanda.example.calc import Calculator | |
NUMBER_1 = 3.0 | |
NUMBER_2 = 2.0 | |
FAILURE = 'incorrect value' | |
class CalculatorTest(unittest.TestCase): | |
def setUp(self): | |
self.calc = Calculator() | |
def test_last_answer_init(self): | |
value = self.calc.last_answer | |
self.assertEqual(value, 0.0, FAILURE) | |
def test_add(self): | |
value = self.calc.add(NUMBER_1, NUMBER_2) | |
self.assertEqual(value, 5.0, FAILURE) | |
self.assertEqual(value, self.calc.last_answer, FAILURE) | |
def test_subtract(self): | |
value = self.calc.subtract(NUMBER_1, NUMBER_2) | |
self.assertEqual(value, 1.0, FAILURE) | |
self.assertEqual(value, self.calc.last_answer, FAILURE) | |
def test_subtract_negative(self): | |
value = self.calc.subtract(NUMBER_2, NUMBER_1) | |
self.assertEqual(value, –1.0, FAILURE) | |
self.assertEqual(value, self.calc.last_answer, FAILURE) | |
def test_multiply(self): | |
value = self.calc.multiply(NUMBER_1, NUMBER_2) | |
self.assertEqual(value, 6.0, FAILURE) | |
self.assertEqual(value, self.calc.last_answer, FAILURE) | |
def test_divide(self): | |
value = self.calc.divide(NUMBER_1, NUMBER_2) | |
self.assertEqual(value, 1.5, FAILURE) | |
self.assertEqual(value, self.calc.last_answer, FAILURE) | |
def test_divide_by_zero(self): | |
self.assertRaises(ZeroDivisionError, self.calc.divide, NUMBER_1, 0) | |
Test Launch
To launch tests from the command line, change directory to the project root directory and run the unittest module directly from the python command:
# Discover and run all tests in the project > python -m unittest discover # Run all tests in the given module > python -m unittest com.automationpanda.tests.test_calc # Run all tests in the given test class > python -m unittest com.automationpanda.tests.test_calc.CalculatorTest # Run all tests in the given Python file (useful for path completion) > python -m unittest com/automationpanda/tests/test_calc.py
Test output should look like this:
> python -m unittest discover ............. ---------------------------------------------------------------------- Ran 13 tests in 0.002s OK
In order to generate XML reports, install unittest-xml-reporting and add the following “main” logic to the bottom of the test case module. The example below will generate the XML report into a directory named “test-reports”.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if __name__ == '__main__': | |
import xmlrunner | |
unittest.main( | |
testRunner=xmlrunner.XMLTestRunner(output='test-reports'), | |
failfast=False, | |
buffer=False, | |
catchbreak=False) |
Then, run the test module directly from the command line:
# Run the test module directly # Do this whenever "main" logic is written to run a test # Examples: XML results file, custom test suites > python -m com.automationpanda.tests.test_calc
Pros and Cons
unittest is “Old Reliable”. It is included out-of-the-box with Python, and it provides a basic, universal test class. Many other test frameworks are compatible with unittest. However, unittest is somewhat clunky: it forces class inheritance instead of allowing functions as test cases. The OOP style feels less Pythonic. Tests cannot be parameterized, either.
My recommendation is to use unittest if you need a basic unit test framework with no additional dependencies. Otherwise, there are better test frameworks available, such as pytest.
7 comments