One of the properties of a valuable test is that it’s fast. While tests with a database might be fast enough for simple projects, their execution times quickly add up. As mentioned in “Running tests in parallel with pytest”, you can run tests in parallel. Nevertheless, even that has its limits. To run more and more tests in the same time frame, you need more and more CPU cores or machines. There’s still your machine. At some point, it will take too much time to run all the tests locally. So you’ll probably skip them. So the question is – can we make our tests for business logic faster? Yes, we can! We can do that by implementing test doubles using contract tests. So, let’s look into that.

Fast vs. Durable

In “Testing Behavior, not implementation details – part 1”, we’ve implemented two stores – an in-memory one and SQLite one. We’ve shown with tests that they both expose the same observable behavior. Can we leverage this somehow? Of course, we can. And we should. We can use the in-memory implementation in tests and SQLite one on live server. Why would we do that? In tests, we want to use the fastest implementation – to speed our tests up. When deployed on the live server, we want the data to be persisted between different deployments and server restarts. That’s why we have to use SQLite implementation. Nevertheless, to really do that we need to make sure that both stores expose the same observable behavior. So how do we do that?

Contract tests

In part 1, we’ve seen that we can use the same tests for both implementations. Yet, the tests were written twice. Having tests written twice decreases our confidence in the fact that both implementations are doing the same. This means that we might change tests for one while forgetting about the tests for the other. This can quickly lead to inconsistencies between implementations. We want to be confident that both implementations behave the same way. So we can switch between them based on our needs. That’s where contract tests come into play. We can use them to execute precisely the same set of tests for different implementations. This way, we ensure that implementations always expose the same observable behavior. So, let’s take a look at an example.

First of all, to do the contract testing, both implementations must expose the same interface. To do that, we must really hide the implementation details. Once we ensure that, we can go to tests. So what do we do? We define the contract – a set of tests – on a class. In this particular case, that’s TaskStoreContract. On the contract level, we define the required fixture for an object that we’re testing, but we don’t implement it. In our example, that’s a store fixture. Then, we use the contract class to create a test class for each implementation. In our case, these are TestTaskStoreSQLite and TestTaskStoreInMemory. These classes implement only the fixture specified by the contract. As you can see, the setup for this fixture isn’t the same. SQLite implementation needs a database connection, while in-memory doesn’t. That’s expected. The important part is that common behavior (after the object is created) satisfies exactly the same set of tests.

Conclusion

Using contract tests, we can effectively implement test doubles. This way, we ensure the same observable behavior for different implementations. For example, this way we can implement in-memory store that can be used in tests as a test double for SQLite implementation. This way we can test our business logic with much faster tests. Also, contract tests force use to really hide the implementation details which improves the quality of our code and tests.

You can find full code examples in this Github repository.

Subscribe To Python Testing Tips

Get Python Testing Tips to Your Inbox

Subscribe To Python Testing Tips

Get Python Testing Tips to Your Inbox

Share This Story, Choose Your Platform!