A significant percentage of writing software is invoking other bits of software that may or may not be in your control. That sounds straightforward, after all most of this code doesn’t even really “do” anything. But as with most things in life, mistakes are made and thus small bugs find their way into your code.
Testing is the obvious answer to this problem. Now before you climb your high horse and start pouring out the bucket of arguments on how good coders don’t need to write tests–and definitely not for simple code–, how testing takes too much time, that your boss won’t let you- or whatever other bad excuse (copied-from-some-Quora-answer-you-may-or-may-not-have-found-on-Google) you can bring up; here is why I think unit testing is valuable:
- Writing tests forces spending some extra time and focus on your code. Just doing that sometimes helps me find bugs, often in the most trivial code. Without testing, this would lead to embarrassing, costly production errors.
- Code under test is code that executes in your control. This helps build confidence, especially with a non-compiled language like Python.
- A well tested codebase is like a coal mine with a thousand canaries. You’ll immediately see the impact of breaking changes.
If your code will be maintained by others in the future, they will value your tests for the same reasons, but in reverse order.
import unittest
def two_plus_two():
return 2 + 2
class SimpleUnitTest(unittest.TestCase):
def test_two_plus_two(self):
self.assertEqual(4, two_plus_two())
Testing “active” code is easy. 2 + 2 should always be 4. Your thermostat should turn on the heat when the temperature drops below X, etcetera. All unit test frameworks have great support for this “transactional” kind of testing, but when all you’re trying to do is verify if something calls something else correctly, test code can become quite unwieldy.
from unittest import TestCase
from unittest.mock import patch
import imaginary_math_library
def two_plus_two():
return imaginary_math_library.add(2, 2)
class SimpleUnitTestMockTest(TestCase):
def test_two_plus_two(self):
with patch.object(imaginary_math_library, 'add', \
return_value=4) as mock_add:
# Test outcome
self.assertEqual(4, two_plus_two())
# ensure 'add' was called correctly
mock_add.assert_called_once_with(2, 2)
Python’s unittest framework offers the mock library, which lets you define fake methods that can verify wether they were called as intended. In the example above, patch.object
temporarily overwrites the add
method on imaginary_math_library
with a mock that always returns 4. When two_plus_two()
is called in the test, it will invoke the mock add
method.
The mock library works great, there are even some nice touches like the patch
context manager–used above–that are a great improvement over building your own mock objects and patching them into code under test. However, when you get serious with it, things become verbose in a hurry. The reason for this verbosity is that it separates mock definition from call verification. Furthermore, mocks have to be “manually” injected or patched into code under test.
I have fond memories from back in the day when I wrote Java (a language normally regarded as one that re-defines the word verbose) of using a framework that offered a quick and simple way of applying mocks in unit testing, Mockito. Mockito uses a builder-pattern-like chained-method syntax that combines mock definition and installation. Verification is still done through separate calls, but despite that, tests written with Mockito are concise and readable.
One can only imagine my surprise and delight when, while looking around for ways to test Python code earlier this year, I stumbled upon mockito-python, a Python implementation of the aforementioned Java library.
Mockito-python (Mockito hereafter) takes a host of great features from it’s Java namesake and throws in a few extra to make things even better. Not only does it combine mock installation and definition in the same statements, expectations get set up simultaneously. This means that calls with unexpected parameters automatically cause test failures. Using Mockito, our little example is implemented this way:
from unittest import TestCase
from mockito import when, verifyStubbedInvocationsAreUsed
import imaginary_math_library
def two_plus_two():
return imaginary_math_library.add(2, 2)
class SimpleMockitoTest(TestCase):
def test_two_plus_two(self):
when(imaginary_math_library).add(2, 2).thenReturn(4)
self.assertEqual(4, two_plus_two())
verifyStubbedInvocationsAreUsed()
verifyStubbedInvocationsAreUsed
will ensure that all mocks defined in a test are used. This is a feature that is absent from the Java library. It only needs to be called once, in contrast to explicit call verification which has to happen for every expected call in a test.
Depending on the nature of your test suite, verifyStubbedInvocationsAreUsed
can be invoked from a tearDown()
method. This is also where you should invoke mockito.unstub()
which undoes all changes applied by Mockito, to prevent the next test from being surprised by side-effects.
Mockito offers a whole host of argument Matchers that can be used to describe parameter expectations and you can use an argument Captor to inspect parameters even further. Mocked methods can return fixed values, raise errors and even execute lambdas if dynamic results are needed in your test.
The whole thing is very well documented and a joy to use. I’ve written a slew of test code with Mockito over the course of this year. Doing so has helped me find a bunch of silly bugs and most of all has helped me build confidence in my code-even the trivial parts!
Since discovering Mockito for Python, my proverbial coal mine is packed to the brim with canaries and I love it! I hope it’s useful to you too. If that is the case, or if you have suggestions, drop me line. Enjoy!