Beware of chicken testing! (or mocks overuse)

Need for mocking

Dealing with problematic dependencies is an indispensable part of software testing. Often, we cannot or do not want to rely on 3rd party service/network communication/hard drive etc., especially in unit-tests. The reasons vary; external dependencies are usually slow, fallible and difficult to put into the expected state before the actual test. Consider the following code snippet (simplified – no logger, no saving to database etc):

…and think how would you test it, assuming that using external services is not an option.

There is no other way but to use so-called test doubles – objects replacing real implementations in tests. Let’s use Mocks from the standard library:

If you do not feel you’re comfortable with mocks yet or spec_set surprises you, check out my (almost) definitive guide about mocking in Python (are you up to date with novelties introduced in Python 3.7 and 3.8, like sealing or unsafe parameter?).

So we should be good to go and safe, right? No.

How Mock can betray you?

The most basic problem with Mock-based tests is that they can give you passing tests even though the code won’t work in QA/Staging/Production environment. It is simply because they are not the same implementation. Even if you are using rigorous spec_set and sealing, Mocks still can still give you a thrill of “excitement” by letting you fall into two specific problems.

Returning value different than mocked implementation

Often as a result of a human mistake of misconfiguring mocks, but this is dangerous because can propagate and waste your time:

Calling methods with wrong parameters

That involves calling it with no parameters when it needs some, with wrong types or with a wrong number of parameters (too many or too little).

type checking is the only reliable solution

The only reliable way (that won’t bite you in the future) is to use type annotations and mypy. If you wonder where to start with it, see my blog posts How to use mypy in my project.

However, a misused solution for the second problem is to make pedantic assertions about all calls:

Don’t get me wrong, making assertions about the way mock was called is bad only in certain circumstances. After all, it is essential to interaction-based testing. But that’s also a slippery slope. Luckily, there are many other red flags…

…when mocking goes too far

Overspecification

If tests are making too many assertions about implementation details we’re dealing with a classic overspefication. In other words, a code under test is literally duplicated in the test. It makes refactoring really hard because whenever you do some slight change, dozens of tests start failing. The same goes for testing private methods.

Regarding the most recent example, when you make an assertion about the way a collaborator object (e.g. payment provider or flights booking service) was called, you introduce a coupling. It will not be an overspecification only if the interface is stable (i.e. is not subjected to change in any foreseeable future)

Mocks returning mocks that return even more mocks

That should speak for itself. Something is very, very wrong with the code you’re trying to test. Such multi-level mocking is just a hack.

Multiple mocks/patches at once

When you see or write a code like this

it may indicate that either the code is not unit-testable or it needs an integration test instead. There’s no good in hacking your tests by mocking half of the codebase (including 3rd party libraries).

Multiple assertions against different mocks in the same test

That’s a classic example of mocking too hard. Naturally, a developer still needs to use a test-double instead of dependency, but they end up with tests that:

  • are very difficult to name precisely
  • can fail for a various reasons

Ideally, each of your tests should fail for only one reason. When you have multiple assertions against different mocks, it’s no longer a case.

Meet stubs

Luckily, there is a relatively easy way out. Mock is merely one of few types of test-doubles. The one I want to bring your attention to is called stub.

A stub is an object that just like a mock can be used instead of a real implementation. Stubs return hardcoded data but one must not do any assertions about the way they were called.

In our test, we once again mock FlightsBookingProvider (because we will be making an assertion about its book method) but instead of PaymentProvider we pass a stub – PaymentProviderFailingStub. It is a simple class that always raises an exception. The stub is not to be used in assertions. It is meant to make it easier to test our code under test (FlightService.book) UNDER CONDITION that payment authorization fails. This shift in thinking allows our tests to be more focused. As a result, they are much easier to name and are also more stable, by failing only for one reason.

If you are a proficient user of Python Mocks, you already noticed that it is actually not necessary to write a class for each stub. You could as well create a Mock and specify side_effect:

Even though we use Mock, payment_provider test-double is technically still a stub, as long as we do not make any assertions about it. So what makes a test-double stub or a mock, is a way how we use it. Perhaps naming class Mock was not the best choice, but now that’s just random rant. 🙂

I believe in the majority of cases using Mock class to create stubs instead of a hand-written class is good enough unless a class looks cleaner to you and your colleagues. A hand-written class has an advantage of being stricter about arguments passed to functions calls, but mypy gives you the same advantage. Plus it would be required anyway to make sure your stub does not violate Liskov substitution principle.

Again, given capabilities of Python standard library Mock, stubbing is mostly a shift in thinking. One way to make sure it’s not lost is to remember about one rule.

Only one mock per test allowed (let the rest be stubs)

Do not verify more than one test-double using Mock in a single test. That enables creation of much more focused, much cleaner tests:

requests stubbing

The idea of stubbing plays very nicely with external services that we use by calling their API. Making assertions whether we called the right endpoint is like… meh. You can kill two birds with one stone – write a stub that will only respond to the expected URL and focus on testing what your code does, given the stubbed response. This can be called a self-verifying mock, it will fail if code under test requests other URL.

This can be both achieved with responses or request-mock libraries. Also, see this recipe for aiohttp.

Summary

Stubs have yet another magic feature – when your starting point is predictable behaviour of dependencies, you start thinking more about valuable testing. Focus is shifted on the code under test. Your end goal is to verify a particular code fragment by checking WHAT it does under specific conditions. Not HOW EXACTLY it does that.

Further reading

4

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.