In this post I will demonstrate how to write unit tests using the unittest module of Python 3.6, which is powerful enough to write detailed unit tests for any project.
Project structure
Begin with the following project structure. All files are empty. I prefer this structure as it allows me to define custom helpers / constants for the project code as well as the tests.
python-unittest
+ project
- __init__.py
- myclass.py
- helpers.py
+ tests
- __init__.py
- test_myclass.py
Basic tests
In myclass.py
create a class called MyClass
, which does addtion and subtraction.
# project/myclass.py
class MyClass(object):
def __init__(self): # We will need this later
pass
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
Then a simple set of tests for MyClass
could be as follows
# tests/test_myclass
import unittest
from project.myclass import MyClass
class TestMyClass(unittest.TestCase):
def setUp(self):
self._myclass = MyClass()
def tearDown(self):
pass
def test_add(self):
self.assertEqual(15, self._myclass.add(5, 10))
def test_subtract(self):
self.assertEqual(5, self._myclass.subtract(10, 5))
Each test case is defined as test_name. setUp
and tearDown
methods are used to define code that is run before and after each test. In this case, a new instance of MyClass
is created before each test. This ensures that tests of functions which change the internal state of MyClass
do not affect the other tests.
To run these tests, run python -m unittest
in the main directory (python-unittest). Both tests should pass.
Switching to external classes
We will now assume that MyClass
has to call helper classes for addition and subtraction. They are found in helpers.py
.
# project/helpers.py
class AddHelper(object):
def add(self, a, b):
pass
class SubtractHelper(object):
def subtract(self, a, b):
pass
The implementation of MyClass
is changed to use the helper classes.
# project/myclass.py
from .helpers import AddHelper, SubtractHelper
class MyClass(object):
def __init__(self):
self._addHelper = AddHelper()
self._subtractHelper = SubtractHelper()
def add(self, a, b):
return self._addHelper.add(a, b)
def subtract(self, a, b):
return self._subtractHelper.subtract(a, b)
Attempting to run the tests will now result in an assertion error similar to this:
======================================================================
FAIL: test_add (tests.test_myclass.TestMyClass)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\dev\python-unittest\tests\test_myclass.py", line 27, in test_add
self.assertEqual(15, self._myclass.add(5, 10))
AssertionError: 15 != None
This is expected as the functions in AddHelper
and SubtractHelper
do not return anything. As we know what both functions do, it is possible to test MyClass
without implementing either of the helper classes.
Patching with mock objects
To test MyClass
, we need to subsitute the helper classes with mock objects. With a mock object, it is possible to create and supply return values for arbitrary properties and methods. This substitution is done via the patch decorator.
Add the follow imports to test_myclass.py
and replace setUp
.
# tests/test_myclass.py
# Imports
from unittest.mock import patch, MagicMock
# Setup Method
@patch('project.myclass.SubtractHelper')
@patch('project.myclass.AddHelper')
def setUp(self, AddHelperMock, SubtractHelperMock):
self._addhelpermock = MagicMock()
AddHelperMock.return_value = self._addhelpermock
self._subtracthelpermock = MagicMock()
SubtractHelperMock.return_value = self._subtracthelpermock
self._myclass = MyClass()
The patch decorator (@patch('module.ClassName')
), is used to replace certain classes in a given module with other objects. In this case, both the helper classes are replaced with MagicMock objects, which provide default return values for any method call.
As the decorators are nested, the mocks are passed in to the decorated function bottom up, so AddHelperMock
is passed in first. With these changes, the tests will fail in a different manner.
======================================================================
FAIL: test_add (tests.test_myclass.TestMyClass)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\dev\python-unittest\tests\test_myclass.py", line 24, in test_add
self.assertEqual(15, self._myclass.add(5, 10))
AssertionError: 15 != <MagicMock name='AddHelper().add()' id='1813015294024'>
The helper classes have been replaced with MagicMock, which return default objects when called.
Making the tests pass again
This is done by overriding the return values returned by the mock objects.
tests/test_myclass.py
def test_add(self):
self._addhelpermock.add.return_value = 15
self.assertEqual(15, self._myclass.add(5, 10))
def test_subtract(self):
self._subtracthelpermock.subtract.return_value = 5
self.assertEqual(5, self._myclass.subtract(10, 5))
This works as self._addhelpermock.add
returns a MagicMock object which has a return_value property which can be overriden with any desired value.
The tests will now pass.
Summary
This post details how to test a python class with external dependencies. The tests themselves however, are rather simple. Part 2 will look at more complex unit tests, where the returned values are more complex and assertions need to be made on the method calls themselves.