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.