Embedded Mocks and Hidden Contracts

Testing is essential. Testing requires diversity, from integration to chaos, from your local machine to production. Writing tests not always is fun because sometimes is hard to write tests because not all contracts are good, and clean and went through a lot of thought processes. Is easy to blame the design, architecture, and even the implementation, but are we sure we are not missing anything? Something that is hard to test very likely will not be tested. Testability is a good property of any design or architecture sure. Making good solutions that are easy to test is a good thing. However, mocking is often taken for granted, meaning is seen as something that is just good and has no drawbacks, but that is not true. Everything has drawbacks. Integration tests are often seen as bad practices and people tend to lean toward unit tests instead. Just keep in mind Integration tests don't require mocks. Unit tests often require mocks to keep them isolated and cut off external dependencies like external APIs, databases, services, and other downstream dependencies. Mocks can be more tricky them people think, they can become forms of hidden contracts. Because of hidden contracts, let's start with the basics.

Unit Testing: Basic Needs

A Unit test is testing an implementation, often a class or function. The test code needs a couple of elements such as:

  • Planning: Know what will be tested and why.
  • Setup: Isolated the code, cutting off external dependencies - done via mocking (but there are other techniques). 
  • Calling the target: Call the implementation, a class method, or a function to be tested.
  • Verifications: Perform assertions to check behavior is according to expected.
The test code is code like the implementation code and should be well-written and easy to make sense of it. Based on the list I just shared we can understand that the test code is like a little orchestrator and is orchestrating two things: the implementation(instances) and the mocks(alongside with the implementation).

Unit Test: Little orchestration between implementation and the Mocks.

Another way to see is that the testing code is doing dependency injection (DI). Another inference or concern we can reason about is, who manages and maintains the mocks?

  • The engineers? Who wrote the implementation. 
  • Where does the mock lives? In the implementation project? In a different project, in a library? 
  • Should we reuse mocks or should we duplicate them?

Regardless if the implementation owns the mocks, the implementation can have empty objects, default objects, and all forms of facilities to make the test code easier and not worry less about mocking. Shipping mocks as part of the implementation code have issues. The drawback of this approach is in runtime or production the mocks will not be necessary, so there will be waste and additional complexity that is not required. 

Such drawback can be bypassed if we have 2 binaries, 1 for the code, the other for the mocks, or simply if the implementation uses a better design that makes it easier to test but also reuse objects. Let's say the implementation is a microservice. The implementation uses DI and has interfaces, and concerns are well separated, in this case, we can implement interfaces for the testing and just inject different objects depending if we are in production or during the testing(which also could happen in prod). 

Another way to see it is basically eating your own dog food. Code can be reused across tests and tests get a bit more clear, since they just need to do basic configuration and not perform crazy mocks anymore. Removing coupling, adding interfaces and testing is not a problem anymore.

Mocks as part of the Test Code

Let's go to a different approach now. Probably the default approach that our industry does most of the time. Testing project is where the mocks live. Such an approach makes the implementation easier, as it does not need to worry about mocks, however for testing becomes more complicated, what if methods are not easy to be tested? 

Mocks as testing project responsibility

The main issue we have here is, what if the implementation is a library, now all the teams will do the same testing. Mocking will be fragile because as the library changes the tests will break everywhere. If you have 100 projects doing mocks and testing your library, migrations will be very difficult. As Google wisely put it: Don't mock types you don't own.

The best thing about duplicating mocks code is that we keep them simple. I keep saying mock but we should be using Fakes and Dummies most of the time instead of mocks. 

Pushing the Mocks to an internal shared library

When mocks are part of the library code, consumers use library mocks as part of their tests something bigger is happening. Sure you can think that you do not want to test the library but you want to test your own code, however, the library is used all over your code, so you can't test your code without testing the library unless you mock it, no problem the library does that for you.


Mocks shipped as part of the library code

Now the internal shared library has its own contract and mocks. Hold on a second, we don't have one contract here, but actually, two because mocks are a form of hidden contracts. 

Mocks can be Hidden Contracts

When you ship mocks as part of your internal shared library, you are coupling your consumers to your mocks, the price will be big during migrations. It is still possible to go with this approach but you need to be aware that you are basically maintaining another contract. 

Mocks now are as important as your main contract. What happens if you change the mocks to support some testing in your internal shared library, well by changing the mocks you can be changing the contract and therefore break the consumers. The best thing to do is to have a clear and nice test interface backed by the mock, this way you know when you will break your consumers or not. 

Again, Mocks are hidden contracts, they need to be maintained. Mocks can turn into bags of cats where there is so much common code to reuse all possible testing cases that the coupling because unbearable. Re-using mocks can be very tricky. Testing the implementation is wrong, creating coupling and refactoring fragility. Tests should be focused on testing the contract, not the implementation.

Takeaways

Mocks are expensive techniques and should be avoided, using simple forms of test double is preferred like dummies and fakes. But when we consider internal shared libraries, microservices, and testing, here is some advice to keep in mind:

  • Test code should be well-written and easy to understand.
  • Avoid coupling to the internal implementation, test behavior, and test the contract.
  • Don't share mocks, don't reuse mocks.
  • Mocks should live in test projects and not share in internal shared libraries.
  • Everything you expose is part of your contract, beware of hidden contracts.
  • Don't mock types you don't own (somebody else internal shared library), ask for testing interfaces.
  • Make testing interfaces clean and as minimal as possible.
  • Testing somebody else library makes migrations slow and painful.
  • Mocks make refactoring painful, avoid coupling to the internal implementation.
  • Do not test if methods are called (yes, forget spy). 
  • Good design matters, good testing matters, and good design makes testing easier.
  • There is a limit to how much design should bend to make tests easier, don't compromise on information hiding and abstractions just for the sake of testing.
  • Hidden contracts make maintenance more expensive and are sources of complexity.
  • Reusing mocks is tricky and easily can backfire.
  • Have interfaces, so consumers can implement dummies and fakes.
  • Think about the tradeoffs: what to expose, how to test, where the code lives, who do it. 

Cheers,

Diego Pacheco

Popular posts from this blog

Kafka Streams with Java 15

Rust and Java Interoperability

HMAC in Java