Introduction
Can you imagine unit tests without a test doubles? How would you test your components in isolation if there were no mocks, stubs, etc.? Modern test frameworks provide variety of choices in this area. Choosing one of them is not a trivial decision, it can determine general approach to unit testing in our project. So which one should we prefer?
To find an answer we could reach for the book “Elegant Objects” by Yegor Bugayenko, in which the author dedicated entire chapter to one particular technique – fakes. In this post I would like to introduce to you the concept of fakes and provide some real life examples written in TypeScript.
Side note: This is third post about TypeScript on our blog. If you are curious why it is so valuable to us, please read the first one. In second one we have explained why TypeScript is especially beneficial in unit testing.
TL;DR;
- we need test doubles to decouple tested object from its dependencies
- mocks are popular test double, but have some flaws:
- introduce assumption about internal implementation of tested object and its relations with dependencies
- often make tests bloated and difficult to refactor
- create temptation to investigate a process, not an outcome
- fakes on the other hand:
- live with a “production” code, they are coupled via interface
- are not limited by any framework or library
- could be used to mitigate problems related to a mocking and to make tests more readable and shorter
Why do we need mocks?
First off all – what are mocks and why do we even need them? Mocks are basically objects created manually or by some library (jest is good example in JS world), which can be used instead of real components to provide fixed responses or to investigate interactions. We can use mocks to skip API calls, mimic database, simplify some time consuming tasks, produce specific responses. It might sound similar to your understanding of stubs or spies but that’s ok for a purpose of that article. However, if you want to know more differences between mocks and other test doubles, I could recommend you this post: Test Doubles by Martin Fowler .
Now we have some common ground, so it’s time to ask what is the reason for mocking? Well, our application is composed of objects and they interact with each other. There is virtually no way to write any real complex application using one single object (unless you are a fan of the God objects). Therefore we have to define dependencies and set up a network of entities working together.
We also should ensure some way to test our code. This is tricky when an object depends on several other objects. We have to somehow “break” that relations, test a piece of code in isolation. Besides, we always should strive to test one single level of code using only a public API. Otherwise our tests would become fragile.
That’s why we invented mocks! Here is a simple example of mocking:
Why mocks are bad
While mocks could be really helpful there are some serious issues related to them.
First, using mocks we assume that tested object interacts with some other object (mocked one) in a specific way. We need to know an implementation details. Our tests are no longer simple “black box” tests. In tests we should use code in a way that other parts of our app will do and inspect only the result.
Otherwise we would violate encapsulation, and any changes to our internals imply changes in tests, even if they are not reflected in public API.
Second, tests are more verbose and less readable. Maybe it was not obvious in previous example, so here is another one:
Did you notice amount of code required to mock a module with pretty simple API? Can you imagine how enormous would be that mock in real life in complex scenario?
Furthermore, while mocking we can even specify exact value returned in first, second and following calls. Below you can see a fragment of the official jest manual:
Sometimes it leads to so complicated mocks, that our tests become unmaintainable (due to amount of code and fixtures hardcoded in test’s files) and unreliable (because we don’t know whether we test our mocks or code).
Finally, when we have a way to define an exact API of a mocked object, we could give in to temptation of investigating not results, but process and intermediate values. Please consider following test:
That is very common – we test usage of mocked object, whereas we should really test an outcome from public API, because that’s how our component will be used in the application.
You should know though, that there are many developers preferring mocks because of their pros. If you are curious about that style of unit testing, this comprehensive post would be a great starting point: “Mocks aren’t stubs” .
What are fakes?
What about fakes? Could they help us? Of course, because there are some clear differences between mocks and fakes.
But what are fakes? In simple words: they are objects that implement the same interface as dependencies used in an application, but with slightly different (preconfigured or fixed, simplified or sometimes even more complex) implementation details. Unlike mocks, fakes are not created by some library, they are “production ready” objects and could be even used in the application. Still pretty general, huh? Let’s see previous snippet rewritten to use fakes instead of mocks:
Now if you know what fakes are, how they could help us in unit testing?
- Using fakes we just provide required arguments to our classes or functions. We don’t assume anything about internal implementation. A class could not even use our fake at all, it doesn’t matter, it requires an object and we provide an object.
- We keep fakes alongside with real implementations. They are always up to date, because they have to implement certain interface. Otherwise a project won’t compile. Therefore, there is simply no way to use outdated fakes in a tests.
- We have no temptation to test a process, because it is encapsulated in a fake. We can just create a fake with specified configuration, provide it to the tested unit and it would (but not have to) use it and produce some result. Thanks to that we are testing only outcome of a public API, not an usage of any dependency. If you need to test relations between entities, use integration tests.
- Tests are less bloated. Fakes live in “production” files, we only import them and instantiate with some arguments.
Fakes in real world
Let’s see some examples of fakes in real application. Those are a little bit simplified (for purpose of the article) pieces of code from production ready systems.
Fake HTML button
Sometimes we depend on native browser API or elements, like HTML button tag. Faking entire button interface would be very cumbersome. It is better to define strict interface on which our component depends. That interface should be compatible with HTML button of course, but could be much smaller and easier to fake. After all usually we don’t need whole functionality of the button tag.
In the same way you can fake a http client in Angular’s services without using any test framework or library.
Fake React component
In React we often render components, which could be used only in certain environment. For example, you can’t simply render Route component from react-router library, if it is not nested inside some kind of router. It means, that in tests, we should either prepare complete environment for tested component or mock problematic child. Wouldn’t it be simpler to provide fake route component, which doesn’t require any parent or context?
Sometimes that pattern is called Overrides – “Better Reusable React Components with the Overrides Pattern”.
Summary
here is very useful technique to test your code in isolation – test doubles. Some of them, while very powerful, comes with some pretty serious flaws, that in practice leads to complex and unreadable tests tightly coupled to the implementation details of the production code.
However you can avoid those issues using fakes, instead of mocks or stubs. They are coupled to the interfaces, not to the implementation, they are always up to date because they live in “production” files alongside with real code, they provide great flexibility and they make your tests more reliable, maintainable and independent from testing platform.