In this article, I will explain what is a Mock in unit testing, something that is getting more relevant in today’s software development environments.
So, if you are a software developer looking for answers, keep on reading. BEON Tech Studio is here to help. Let’s dive in!
A Mock is probably not what you think.
In my experience, developers tend to call Mock any object used to substitute dependencies when writing unit tests. Mocks are just one type among several types of substitutes. We call these substitutes Doubles.
Why is it important to make these distinctions? Because giving names to specific concepts expands our language and consequently our ability to structure thoughts and communicate ideas.
Gerard Meszaros in his work XUnit Test Patterns uses the word Double to encompass all object types that override dependencies. And he describes five different types:
Dummy Object. Let’s start with the simplest of all. A Dummy is an object with no executable code, it could even be a “null.” It’s simply there to satisfy the parameter of a method or constructor.
The dependency the Dummy replaces isn’t exercised in the test, so its content doesn’t matter. For example, we can test if our SUT (System Under Test) enters the first “if” of our method and then returns a value. The test won’t get to call the dependency that could be below. So there is no need to add a complex object—a Dummy would suffice.
Fake object. It’s a simplified implementation of a dependency with little to no side effects, thus faster than the actual implementation. The main motivation for using Fakes in our tests is to replace hard-to-manage dependencies. A perfect example is an in-memory database.
In general, it’s better to avoid this type of Double. A Fake, by definition, is different from the real implementation. This means that it can give us unexpected results if we do not know how it works inside. If we wanted the same results as the real implementation, we would need an exact copy, and it would no longer be a Fake.
Before continuing with Stub, Spy, and Mock (the most important Double types), it’s necessary to understand that there are two main forms of verification in unit tests. State verification and behavior verification.
State verification examines “WHAT” changed. For example, we could check if an object mutated correctly by examining its public properties. We could also make sure that the final result a method gave us is what we expected.
Then we have the behavior verification, which is in charge of verifying “HOW” our SUT interacted with the dependency.
When we do a state verification, we only care about what changed, but we don’t care about how it changed. This distinction can be important when refactoring code. A refactored code should have the same functionality even if the structure of the code has changed. That is, with a refactor, the how is changed without affecting the what.
If our tests only check the what (state verification), we can refactor without worrying. Our tests will leave us assured because we will be able to make all the refactors we want, and our tests will inform us if we break any functionality. On the other hand, if our tests know “the how” (behavior verification), they will break at the first refactor even if the functionality is the same.
For this reason, I recommend giving preference to state verification over behavior verification. This does not mean that behavior verification should be discarded. On the contrary, there are situations where it’s the best tool available (or even the only one).
Let’s take an example. We have a method that depends on a REST service, and it needs to POST to the service. If everything went well, our method returns a boolean informing us whether the operation was executed correctly or not.
How can we verify that our method executed successfully without calling the actual dependency?
If we only check the state, that is, if we only look at the boolean that the method returns, we would be fooling ourselves. It would only prove that our method returns booleans, which would not be very useful since it doesn’t tell us if we call our REST service with the correct parameters.
This is where behavior verification comes to the rescue. We can use a Double that keeps track of the called methods, the number of calls, and their respective arguments. Finally, when the execution of our SUT is finished, we verify those records to prove that the behavior has been the desired one.
With this clear, let’s define the remaining Doubles.
Stub. This type of object is used when we only do state verification. The Stub is a simple object that replaces a real dependency. Its purpose is to be called within the SUT and return the values needed by our test.
Spy. A Spy is similar to a Stub, but besides being able to verify the state with it, we can verify behavior. Unlike a Stub, the Spy keeps track of how many times each method was called and, if necessary, the arguments it received. When the SUT finishes executing, we go and assert the Spy records to verify that the behavior has been as expected.
Mock. A Mock is also used to verify behavior. Its main disparity with a Spy is that, with the Mock, we define expectations for each test, and if these expectations are not met, our test will automatically fail during execution or when calling a Verify method, without the need to manually explore the Double’s state as we would with a Spy. We could say that a Mock is a Spy with superpowers. Generally, a Spy is created manually. However, to use Mocks, there are many libraries that allow us to focus on our tests and not on Doubles maintenance.
I hope my explanation has been helpful. I highly recommend the definitions that Gerard Meszaros shares about everything related to XUnit. I also highly recommend reading Mocks aren’t Stubs by Martin Fowler, which inspired me to learn more about the subject. And finally, The Little Mocker by Robert C. Martin explains these same concepts in a conversational form.
“Impossible is just an opinion.” – Paulo Coelho Were we afraid? Yes, many times. Was it easy? No, it was not easy at all. Still, what we achieved in 2021 left us with many lessons about what to do when you think something is impossible, about success, teamwork, fear, and much more. Let me show…
The photo above shows our team from Buenos Aires, 25 of the nearly 100 BEONers from all over Latin America. We believe in connecting the best talent in our region with high-end companies in the U.S. Through challenging projects, extraordinary teammates, and a friendly and supportive company culture, we propel every BEONer’s career in new…
We have something very clear: success does not consist of making ourselves the best, but in helping those around us become better. Based on this win-win mentality, Lead Software Engineer Enrique Arrieta and Co-Founder Michel Cohen gave the BEON team a personal finance workshop in which they spoke on how to put savings to work,…