Python Unittest

Write Better Unit Test

A single unittest case should have:

  1. Cover only one path through code.
  2. Have better asserts and documentation.
  3. Providing informative failure messages.

To avoid reinventing the wheel, using test fixtures:

test fixture: represents the preparation needed to perform on or more tests, and any associated cleanup actions. This may involve, for example, creating temporary or proxy databases, directories, or starting a server process.

graph TD;
  setUpModule --> setUpClass;
  setUpClass --> setUp;
  setUp --> test_*;
  test_* --> tearDown;
  tearDown --> setUp;
  tearDown --> tearDownClass;
  tearDownClass --> setUpClass;
  tearDownClass --> tearDownModule;

Write Testable Code

Some important techniques to make code easier to test:

  • Documentation.
  • Dependencies.
  • Decomposition.
  • Graceful and informative failure.

Google Blog Writing Testable Code.

Dependency Replacement

Test Double: A simplified replacement for any dependency of a system under test.

You should use test doubles if the real thing:

  • Isn’t available
  • Won’t return the results needed
  • Would have undesirable side effects
  • Would be too slow

Types of test doubles:

What it does When to use
Placeholder Does nothing.
Passed around but never used
You need a placeholder.
Stub Provides canned answers. You want the same result every time.
Spy A stub that remembers
how it was called.
You want to verify functions were called the right way.
Mock Configurable mimic of
a particular object.
Can behave like a dummy, stub, or spy.
Fake A simplified version of
the real thing
Interacting with a complicated system.

Other Fakes

You can find on Internet such as MySQL fake, etc.

Unittest Module

Python unittest module: The python unittest module: https://docs.python.org/3/library/unittest.html#module-unittest

Basic example: https://docs.python.org/3/library/unittest.html#basic-example

How to organize the code: setUp()/tearDown() for each test case: https://docs.python.org/3/library/unittest.html#organizing-test-code

Also it has class and module level fixtures: https://docs.python.org/3/library/unittest.html#class-and-module-fixtures

Assertion methods: https://docs.python.org/3/library/unittest.html#assert-methods

Assert methods allow custom messages, for example:

1
2
3
4
self.assertEqual(
10, call_count,
'The call count should be 10, not {}.'
.format(call_count))

Command-Line interface: https://docs.python.org/3/library/unittest.html#command-line-interface

1
2
3
4
5
6
7
8
9
10
11
12
# see list of options
python -m unittest -h

# -v: verbose
python -m unittest -v test_module
python -m unittest test_module1 test_module2
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method

# -k: pattern match with substring
# --durations: show N slowest test cases
python -m unittest -v test_module -k foo --duration 5

test_module is a python file, such as my_module.py and my_module can be imported by other python programs.

Unittest Parameterized

Parameterized module, installation and examples: https://github.com/wolever/parameterized

When using with mock.patch decorator, the order matters, for example: https://github.com/wolever/parameterized?tab=readme-ov-file#using-with-mockpatch

1
2
3
4
5
6
7
8
9
10
11
import unittest
from parameterized import parameterized
from unittest import mock

@mock.patch("os.getpid")
class TestXXX(unittest.TestCase):
@parameterized(...)
@mock.patch("os.fdopen")
@mock.patch("os.umask")
def test_method(self, param1, param2, ..., mock_umask, mock_fdopen, mock_getpid):
...

Unittest Mock

Unittest mock library, it is extremely important: https://docs.python.org/3.8/library/unittest.mock.html#

Usually Mock, MagicMock and patch decorator are enough for most cases, please check quick guide for quick onboarding: https://docs.python.org/3.8/library/unittest.mock.html#quick-guide

There is a separate document for using mock, it is more advanced: https://docs.python.org/3.8/library/unittest.mock-examples.html#

A mock object can pretend to be anything. It can:

  • Return expected values when called.
  • Keep track of how it was called.
  • Have attributes.
  • Call other functions.
  • Raise exceptions by side effect.

A Mock object can have assertion for how it has been used, for example:

1
2
3
4
5
6
7
8
mock = Mock()
mock.method()

mock.assert_called()
mock.method.assert_called_once()

mock.method(1, 2, 3, test='wow')
mock.method.assert_called_with(1, 2, 3, test='wow')

The mock assert methods are under Mock class with examples: https://docs.python.org/3.8/library/unittest.mock.html#the-mock-class

Some other examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
m = mock.Mock()
# Set up return value.
m.return_value = 'foo'
_ = m()

# Tracking calls and arguments
self.assertTrue(m.called) # bool
self.assertEqual(2, m.call_count) # int

# With what args called?
# All calls
m = mock.Mock()(return_value=None)
m(1, 2, 3)
m(4, 5, 6)
m()
expected = [mock.call(1, 2, 3), mock.call(4, 5, 6), mock.call()]
self.assertEqual(expected, m.call_args_list)

# Most recent call args.
args, kwargs = m.call_args

# Check most recent calls usage.
m.assert_called_with('foo', a=1)
m.assert_called_once_with('foo', a=1)

# Check other calls.
# Use mock.ANY as a placeholder.
self.assertTrue(m.assert_any_call('foo', a=1))
self.assertTrue((m.assert_has_calls([
mock.call('foo', a=1),
mock.call(mock.ANY, a=1),
])))

Mock vs Magic Mock

Basically, MagicMock is a subclass of Mock with default implementations of most of the magic methods such as __init__. You can use MagicMock without having to configure the magic methods yourself.

Return Valud Vs Side Effect

The return_value is static, but side_effect is versatile, it can configure not only return value dynamically but also arising exception, please see examples: https://docs.python.org/3.8/library/unittest.mock.html#unittest.mock.Mock.side_effect

they can be used together.

Patch Decorator

This is a commonly used technique, eapecially the object patch, for example, in your method you call a class instance method, the patcher can help set up the return value or side effect for it.

For object patch, the object you specify will be replaced with a mock (or other object) during the test and restored/undo when the test ends:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# the autospec=True: the mock will be created with a spec from the object being
# replaced.
@patch.object(
SomeClassorModule,
'class_or_module_method',
autospec=True,
return_value="hello",
side_effect=xxx)
# the patched object is passed as an extra argument to the decorated function
# here it is "mock_method".
def test(mock_method):
ret = SomeClass.class_method(3)
mock_method.assert_called_with(3)
assert ret == "hello"

Order when multiple patch decorators, it is bottom-up:

1
2
3
4
5
6
7
from unittest.mock import patch

@patch('module.ClassName2')
@patch('module.ClassName1')
def test(MockClass1, MockClass2):
# note the order in signature: MockClass1 the bottom one first.
pass

Helpers

The commonly used helpers:

  • mock.ANY
0%