Introduction
These are tools used to imitate or substitute parts of the production code in our testing environment. Usually Services, Repositories, event buses, etc. although you can apply the same principles in much simpler contexts (like katas). They are useful to ensure we are testing the different parts of the system in isolation.
In practical terms, we can say that when the system under test (SUT) depends on a separate piece of our application (separate as in should be tested separately), the second one should be substituted. Specifically the parts we are not interested in testing but are required for our SUT to function.
As was the case for the tools we saw on the previous post, these Doubles might take a minute to set up, but not using these tools makes our tests significantly more fragile and less reliable, since when they break we wonāt have a clear picture of who exactly is at fault.
In the end, Doubles are just a false implementation of production code. Say you want to test UserService
, but it depends on and requires a UserRepository
with a search()
function. You would make an InMemoryUserRepository
that implements UserRepository
(with its search()
function) to test the Service independently of the Repository.
That InMemoryUserRepository
is a sort of Double. Weāll use this common example going forward.
Note on definitions
The terms Mock, Spy and Double are often used in different ways depending on the source material
When naming things in your code base please make sure there is a consensus within the team regarding what each word refers to. When looking for help online or debating with a co-worker, ensure you understand what they mean by these concepts. You might be using the same words but talking about different things.
Some consider Stubs to be very different from Fakes, some donāt include Dummy as a testing Double, and some donāt differentiate between Mocks and Spies. Some just consider everything a type of Mock Object.
So hereās more fuel to the fire.
Good luck š
Dummy
False implementation of production code with no real behavior. Itās literally there to make your code compile.
Use Case
You would use a Dummy to substitute a dependency of your SUT when itās only needed at compile time but doesnāt really do anything in your testing scenario.
For Example, if our InMemoryUserRepository
were a Dummy, it would implement the production UserRepository
and have a search()
function that does nothing (or the absolute minimum to compile).
Fake
False implementation of production code with very basic, test specific behavior. It would receive some starting data to simulate operations.
Use Case
A Fake is useful whenever you need some very simplistic behavior.
For example, if our InMemoryUserRepository
were a Fake, it would implement the production UserRepository
and have a search()
function that actually implements production logic, but searches in an Array that it got via constructor (starting data).
Stub
False implementation of production code with basic, use case specific and re-usable behavior. It would use some hard coded data to simulate operations.
Use Case
Use a Stub when you need some basic test independent behavior. Basically as soon as you use the same Fake twice with the same starting data, build a Stub with that starting data.
Following our example, instead of our previous InMemoryUserRepository
, you would build an InMemoryAdminUserRepository
that implements the production UserRepository
and has a search()
function with production logic, but searches in a predefined, hard coded Array made up of a bunch of random Admin Users. This hard coded Array substitutes the starting data from the Fake example.
This way you could use the same Stub in multiple tests without rewriting the Array and ensuring they all work with the same data.
Spy
Piece of code (or external library) that allows the tester to check if and how a specific interaction with the spied code has taken place. It can tell the tester how many times its methods were called, what parameters were passed to each of them, in what order they were called, etc.
This is the first concept that only takes into account behavior, disregarding output.
It is also the first tool that allows us to see the inside workings of the systems we are testing. Useful, but prone to coupling. Use them sparingly!
Use Case
One would use a Spy to further detach the SUT from its dependency and/or to only make assertions regarding the interaction between the two, not really caring about the final output.
Common scenarios are the assertions āif the function was called at least onceā or āif the function was called with X argumentā. Youāll tend to find this behavior implemented within other Doubles, since it is often insufficient by itself.
So now, our InMemoryUserRepository
would implement UserRepository
and a search()
function that literally does whatever, as long as it notifies the tester that it was called.
In our very simple example, the tester could ask the InMemoryUserRepository
for the state of searchHasBeenCalled
and use that to assert the expected behavior, no matter what the search()
function actually does.
You can hopefully see that this couples our Service test to how our service functions (we test if it calls a given function), rather than to what it actually achieves (simply testing the output).
Mock
Keep in mind that, as noted before, some literature refer to everything weāve seen here as āmock objectsā. That being said, the sources Iāve found that consider them as its own specific thing all agree on Mocks being the more sophisticated of the lot.
Speaking of sources, the actual source material states:
[Mock Objects] replace domain code with dummy implementations that both emulate real functionality and enforce assertions about the behaviour of our code.
Similarly, Martin Fowler describes them as:
Objects pre-programmed with expectations which form a specification of the calls they are expected to receive.
One major benefit of using them is:
It makes it a lot easier to write a mocking tool.
Thus, they are often provided by an external library.
Mock objects give the tester full control over the behavior of the code being mocked, which can even be manipulated dynamically. It usually offers all the benefits from the previous tools as well.
As you might imagine, this can be as complex to implement as one heartās desires (hence the external library), but they are very useful and easy to work with.
A super simple Mock might look a lot like a Spy that, instead of exposing whether a given function was called, has some sort of assert
function. Mocks āknow what they are testingā, they make assertions on their own.
That being said, things can (and usually do) get more complicated than that.
Use Case
Say you want to simulate some specific complex behavior of our UserRepository
to see how the UserService
responds. Given a complex enough behavior, you might have to duplicate (and maintain) quite a bit of code or give up completely and test both elements together.
This might put you between a rock and a hard place, having to choose between flaky tests or giving up test isolation.
You can use a Mock to abstract that complexity away altogether. In fact, if using an external library, you wouldnāt even be implementing a substitute for our Repository (like the InMemoryUserRepository
from before), since those usually provide a way to create mocks on the fly based on the interface it should implement.
Example
Suppose that, when our UserService
calls the UserRepository
implementation, the Repository needs to go fetch some data from the database, wait for an email to be sent, check for authentication with a third party service and call your mom to say you love her. Then, based on the results, the Repository returns either an empty array, an array with 4 elements or null
(which as it turns out is the behavior you need for your use case).
You could āre-implementā all that code/behavior, or you could mock the whole thing. With Mockito for example you would annotate the Repository with @Mock
and use it like this:
A lot simpler than the alternative! Although you are adding a dependency to your tests. Pick your poison!