I’ve seen a lot of examples where mock objects are badly used in Python unit tests. A lot of people seem to be guilty of only reading the first few paragraphs of the unittest.mock documentation, resulting in some very fragile tests that end up just being a cause of frustration.
My background is primarily as a Java developer, so when I picked up Python I was quite shocked by how lax Python mocks appeared to be, if you feel the same then this may help.
Some people will strongly disagree with mocking in this way, and that’s ok! I believe there’s a time and a place for mocking to this extent, and that it’s a judgement call as to whether you use autospecced mocks, a more convincing test double, or maybe even the real object.
While on the topic of testing, I strongly recommend Obey the Testing Goat
So we have an object we want to use, perhaps it’s from a library and changes often but currently it looks like this:
gist:MGough/c0eff7aa71c1a316bffc35cd942c12b6#example_object_someone_else_implemented.py
We also have our object that we want to test:
gist:MGough/d7cfe4cdffa0c50cd891fabde3178ff8#example_object_to_test.py
Looks like we’re going to need to mock SomeClassOutOfOurControl. Here’s an example of the most common ways I see it done, probably because it’s how a lot of other blogs tell you to do it:
gist:MGough/24e2670a9656c8c19811965ed0c9f24f#test_example_object.py
Yay mocks! But this is fragile… If there is a typo in our class or an interface change in the imported class then the tests can still pass. Swap out the imported class for this one and watch as magically nothing changes:
gist:MGough/08ef5a3dd9339d12ef4b2d94f13a6f72#example_object_someone_else_implemented_v2.py
Despite the fact we call SomeClassOutOfOurControl.add() with 2 arguments the tests continue to pass when it only accepts one.
There’s a super simple solution to this, and it’s called autospeccing. This means that your mocks are limited to have only the method which exist on the class being mocked, including the number of arguments. It can also handle attributes, but those are a special case, as those defined in functions aren’t included so I’d strongly recommend reading the docs.
gist:MGough/8894e0f7cdcb21b11714ed4be46e15e2#more_reliable_mocks.py
Now we have mocks that are slightly more resilient! This of course doesn’t change the need for other levels of testing (integration or component level testing) but it makes our unit tests a bit more sensible.
If this seems like it would be useful to you then I highly recommend reading the unittest.mock documentation thoroughly. If you’ve received a technical test and there’s a chance I will be reviewing it then I’d definitely recommend reading the documentation, bonus points if you post a correction to this blog :).
Cover photo by Maarten van den Heuvel on Unsplash