Don't Mock What You Don't Have To
Embrace sociable unit tests and the principle of only mocking the API layer.
New Testing Principle
At my team at DFINITY, we use a principle in our unit tests:
Don’t mock what you don’t have to.
My colleague David de Kloet suggested this principle when he joined the team. He meant mocking or stubbing only the API layer in our project but letting the rest work as expected. By mocking, I mean to change the behavior of that layer instead of letting the code inside it be executed during the test.
Since this change, our test coverage has improved a lot. We find fewer bugs before releases, and I feel more confident when refactoring.
Sociable and Solitary Unit Tests
There are two main types of unit tests: sociable and solitary.
Solitary unit tests are those tests where all the dependencies are mocked or stubbed.
Sociable unit tests let the part being tested use the dependencies up to a certain point that are too slow or non-deterministic. A common limit is external dependencies like databases, filesystems, servers, etc.
Problems With Solitary Tests
Before we applied this principle, we wrote solitary tests, but not 100% solitary. Some dependencies were not mocked because we didn’t think it necessary. We didn’t have a hard rule on what to mock and what not to mock.
Not having a hard rule was the main problem. We had to go deep into the test implementation when a test failed after a change. We had to look into what was mocked or stubbed and how the error could be in the setup, a missing stub, a wrong mocked data, the component itself, etc. There were more layers that could go wrong.
Moreover, mocking or stubbing many dependencies came with its problems. For example, we might make a mistake and set a mock with nanoseconds when it should have been milliseconds.
Embracing Sociable Tests
Now, we embrace sociable unit tests and the principle of only mocking the API layer.
For example, when we test a component, we don’t mock the state management layer, helpers, or child components. Only when the tested component makes an API call can we mock that part and return some expected data.
It helps that our API layer is clearly defined and easily mocked.
To be exhaustive, the API layer is not the only one we mock. We also mock:
Logged-in users.
Connections to a hardware wallet.
Timers and clocks to add determinism.
Why I Like It
First, we have a clear rule everyone follows, and we don’t spend time deciding what to mock or not.
Second, it’s easier to do the setup of a test. There are fewer assumptions to make; we identify the API calls and mock or stub them. An easy setup is especially helpful when testing large orchestrator components like pages.
Third, we mock fewer data types, which means fewer mocks to maintain.
Fourth, we started doing integration tests using the same framework for unit testing. For example, we use the accounts page to perform a whole user story, like a token transaction. It’s like a simple version of an end-to-end test that requires less setup and runs much faster.
Fifth, it improves my knowledge of the project. When something fails, I follow the path of the code, and I know that everything runs until I reach the API call. I don’t just fix the test or think on mocks; I walk the project.
Thanks
I have embraced sociable tests and the principle: “Don’t mock what you don’t have to.”
Thank you, David, for enlightening us with your wisdom.