Why test doubles?
Dependencies are an integral part of most codebases. They come in different forms:
- internal dependencies: modules (functions and classes) used by the rest of the code
- external dependencies: modules exposed by libraries, frameworks etc
- physical dependencies: file system, database, network connection etc
The goal of unit testing is testing modules (units) "in a vacuum" in order to quickly and accurately identify the root causes of bugs. To do so, we have to replace dependencies with test doubles.
Test double types
There are various types of test doubles:
spy. Although they are often used interchangeably in practice, in theory those terms describe different concepts. Gerard Meszaros was the first to elaborate on the subject in his 2007 book xUnit Test Patterns: Refactoring Test Code. According to his terminology:
Stubsare used when we want to emulate existing functionality by providing our own implementation.
Example: A stub may be used to replace a function that fetches records in a database table by their id. Instead of the database, our method looks in an array of predefined objects used as test data.
Fakesare like stubs but with a very simple (dummy) implementation.
Example: Same as above, but instead of looking at an array and returning a matching item, we always return a specific object that suits our test's needs.
Mocksare used to make assertions on function calls. Assertions are made before we invoke the functionality being tested.
Example: we want to test that an email is send every time a button is pressed. We don't want to send an actual email, so we
- Mock the email sending function
- Assert that it will be called with the correct data
- Invoke the functionality being tested
Spiesare also used to make assertions on function calls. The difference with
mocksis that assertions are made after the test takes place.
Example: Same as above, but the assertions will be made last, after the functionality being tested has been invoked.
Sinon.js supports all test double types mentioned above:
Stubs support both functionality replacement and call assertions.
Fakes are almost identical to stubs. The difference is that the
fakeAPI does not provide any module replacement functionality, which can "plug" them in the unit under test in place of the original module. This API is available for stubs and spies.
Mocks support both functionality and call assertions. The main difference from stubs is that assertions must be made beforehand.
Spies support call assertions.
A summary of test double features in Sinon:
|Functionality replacement||Call assertions||Assertion order||Module replacement API|
When to use each?
Sinon stubs are, to my opinion, the most powerful and useful test double type in Sinon. They can be used in place of every other type, and I find myself using them more often than any other type.
Fakes are very similar to
stubs, both in unit test theory and in their Sinon implementation. As mentioned in the previous section, their theoretical difference is that fakes provide tests with dummy values directly, instead of trying to emulate the logic of the mocked functionality.
In practice I have never found a reason to use a
fake instead of a
stub. The semantical/theoretical distinction between them is negligible, and often not even clear in real-life unit tests. Usually the way a stub/fake is implemented is not black and white, but a gray area: some parts of the original functionality are accurately simulated, while others are just ignored or naively implemented to serve specific test needs. Moreover, one could argue that the degree of sophistication of stubbed methods should be considered an implementation detail.
Since their functionality and semantics are so similar and stubs are more powerful, I prefer them over fakes.
The Sinon implementation for Mocks gives them equal power to stubs, by supporting both functionality replacement and call assertions. Unit test theory places emphasis on the latter. For me, the final decision is based on two factors on which I will elaborate below.
The Sinon docs state that
Mocks should only be used for the method under test.
I am not sure if the above is a widely accepted fact in unit testing theory. Moreover, theory does not seem to prohibit stubs from being used for the unit under test.
One could argue that it is useful to differentiate between a dependency and a unit under test being replaced. In practice, the second scenario is not common at all. The point of putting a unit under test is to test its actual logic, so we would fake parts of it only as a last resort.
Faking functionality of the unit under test seems to be a bit more common in languages like Java, when an abstract class or static method needs to be tested. Again, there are ways around this without mocks:
- We can test an abstract class using a dummy subclass
- We can test the inherited API of an abstract class when testing its subclasses. This has the advantage of being closer to how the code is actually used
- Some test guides such as the Google Test Guide suggest against static methods altogether
It should be noted that the term
mock seems to be more prevalent in the Java testing literature. In fact, it is used in the names of the most popular Java test double libraries: Mockito, JMock, PowerMock.
Should assertions be made before or after the unit under test is invoked? This is a matter of preference. I personally prefer making assertions after test functionality is invoked, since
- It is consistent with assertions in all other test double types
- We can leverage Chai's BDD assertions (
expectsyntax), which reads very fluently
stubs instead of
Sinon stubs support the full test spy API, which means that they can be used in place of spies. I prefer not to do that because their semantics differ significantly:
- In stubs, the actual functionality is replaced
- In spies, the actual functionality is retained, and we just make call assertions
I hope that this article has made the distinctions between different test doubles clearer! Unit testing theory on the subject seems to be a bit blurry, which may create confusion on which approach to take. We can only hope that the accumulation of practical knowledge will result in future developments in the theoretical field, which in turn will be fed back into testing practice. 🤞