Contract testing and test doubles
When we talk about contract testing, people tend to think this is something to do between services that talk through HTTP. Contract testing…
When we talk about contract testing, people tend to think this is something to do between services that talk through HTTP. Contract testing is much more than that, it’s a way to be sure that every part of your software works correctly with others without paying the cost of creating e2e tests.
In mechanics, gears never work alone. They transmit force directly (arranged in gear trains) or with couplings. Similarly, software components are always dependent, exchanging messages or changing observable state.
Don’t break the contract chain
Test doubles are replacements of the real components that imitate how those other components work. But how can we validate that this is really happening, this is what contract tests tries to solve.
What is a contract?
A contract is a legally enforceable agreement between two or more parties that creates, defines, and governs mutual rights and obligations between them. A contract typically involves the transfer of goods, services, money, or a promise to transfer any of those at a future date. In the event of a breach of contract, the injured party may seek judicial remedies such as damages or rescission. Contract law, the field of the law of obligations concerned with contracts, is based on the principle that agreements must be honoured.
wikipedia
In fact, if you think about it, when you use a test double there are three parts, the client of that test double (the service under test SUT), the real implementation and the double itself.
The client is assuming different things of its collaborator, these assumptions will be verified with the test double. Because our test is using the test double for checking our SUT.
This is the problem devs that don’t like to use test doubles always put over the table to do integrated tests. The fact that you are only testing against doubles, contract tests helps you to avoid that problem. Anyway, as this is a new set of tests we should minimize the number of integrations we need to check with our contract tests, this is why I use them between my layers.
Integrated tests
Integrated tests are a scam — a self-replicating virus that threatens to infect your code base, your project, and your team with endless pain and suffering.
Integrated tests are a scam
E2E tests are integrated tests, let’s minimize them, to make them irrelevant. One way to work in this direction is to use contract testing instead of them, another one could be to design our system with this in mind.
Top-down design
Then we need to do something to check that the real implementation and the test double behave the same. Both things will implement the behavior our SUT is interested in.
Let’s think that we do top-down design, and we will create our test double before creating the real implementation (because we want to have early feedback about our upper component).
Our test double in fact is another implementation of the behavior we want to create, a simple and easy implementation. The best way to have two implementations of a behavior is creating an interface. That interface will also be implemented by the real implementation.
The inputs and outputs of our test double will need to be checked against the real implementation, this is our contract test.
Let’s use as an example a repository to save and retrieve positions and directions in a db, this is the interface of this example:
Then let’s create a contract test to be sure that the real implementation and the test double are behaving the same:
This is an abstract class with all the test that each implementation requires to pass, each of our different implementations (the test double, and the real one will extend this class to explain how to create the PositionDirectionRepository).
Yes, I have tests for my “InMemoryPositionDirectionRepository” that is in fact a “fake” implementation of the “PositionDirectionRepository” interface. But these contract tests validate that my fake and my real implementation pass the same set of tests.
And those tests were created thinking on the needs of my client (the consumer of that interface).
But why to use this instead of E2E tests?
Good tests
Let’s remember what the FIRST principles to understand the good properties of a test:
Fast
Isolated/Independent
Repeatable
Self-validating
thorough
Contract tests are just tests validating junction points, the points where we will use doubles. We said that we will try to minimize the use of test doubles, we will try to use them just in the different layers of our code.
So seems that contract tests are faster, less flaky (self-validation) and easier to set up than e2e, also you can write them faster and use them to reduce your feedback loop.
Apart from that, a contract tests that fails will always explain what part of the contract has been broken and if it was broken in the test double or in the real implementation (more isolated).
Kent Beck : “I get paid for code that works, not for tests”
stackoverflow
There is no much value having tons of tests testing exactly the same code once and again, having a lot of setup to allow the test to achieve the correct state of your application. This is one of the reason I prefer contract tests over e2e because they help me to write the minimum set of tests to build my application.
In fact, this decision of using test-doubles only between layers of code that you control gives you a way to use the feedback from your tests to design your code.
Use test doubles to delegate decisions for the future, if you yet don’t know what db to use or how the final rest-endpoint will be. Doubles will help you to create wrappers over those concepts that isolate you (at a level) from all the specific decisions that are not important to build your business logic.
Use doubles to reduce the complexity of your production code, so to reduce the number of tests you have to create in your SUT.
There are devs that try to avoid the usage of test doubles, they try to test everything with real libraries and infrastructure. I don’t like that approach because the effects of having a lot of integration or e2e tests are Low Feedback Loops (very slow pipelines). People tend to create big batches when the test feedback loop is slow.
When to use test doubles