Mockingbird: The Why, When and How of Testing with Mocks - Part 1

Gavin Rifkind
Wix Engineering
Published in
5 min readOct 27, 2021

One of the most important tools for writing high quality, bug free software applications is tests. Whether using TDD (Test Driven Development — writing tests first) or writing tests after the development, it is clear that tests improve the quality of the code, and decrease the amount of bugs. One of the obstacles to writing good tests is code interdependencies that exist in any software system. In this 3 part series, I discuss the use of mocking as one of the ways to overcome the difficulties of writing clear and concise tests, even when the code has many dependencies.

Northern Mockingbird photo courtesy of Ryan Hagerty, US Fish and Wildlife Service

Introduction

When writing tests as part of the software development process, it is very important to keep the tests clean, structured and readable, so that they communicate their intent simply and clearly. In most cases, the structure of a test should have 3 sections — the setup of the “current state” of the system, the call to the tested function, and the verification to make sure the function did what was expected. This test structure is known as GWT (Given When Then) or AAA (Arrange Act Assert).

Given / Arrange — this is where the “current state” is arranged.

When / Act — this is where the function under test is called.

Then / Assert — this is where the results are checked for correctness.

Using this structure, the tests clearly document the preconditions, intended interaction, and the postconditions of the system.

A simple example of GWT format

In the first part of this series, I discuss mocking, and how mocks can make the setup and assertion sections of the tests a lot easier to implement, and help with writing code that is easy to test. I also look at some of the reasons to use mocks. In the second part I compare some of the mocking libraries and other strategies that we use in the Server Guild at Wix, and in the third part, I discuss some of the pitfalls of using them.

What is a mock?

In the literature mocks are also referenced by other names for example “stubs”, “spies”, “fakes” and “dummies”, depending on their behaviour. The meanings and differences between these names are not consistent, and therefore in this article I will use the term mock to refer to the collective group of all “test doubles”.

Classes usually have dependencies on other classes, known as collaborators. A mock is an object that is used in tests as a “stand-in” for a real collaborator that the class under test depends on. Mocks are typically used to simulate the state and behaviour of the collaborator that they are mocking, by returning predetermined values. The mock object has the same interface as the real class, so the code that is being tested does not know that it is calling a mock object instead of the real one. Simply put, a mock can be seen as a replacement for a real object that has a fake implementation. It responds to the same function calls, but with responses that can be controlled by the test code. They are similar to crash test dummies in the car manufacturing industry, that take the place of real humans in order to test the vehicle. Another analogy could be stunt doubles in movies that take the place of the real actor, in dangerous situations that could harm the actor.

Why do we need mocks?

Setup Stage

Tests usually depend on some complex state, which is difficult to set up. Mocks are used to give the developer the ability to define the state and behaviour of the collaborators, by specifying which functions of the mock will be called, and what the expected parameters will be. It is also possible to specify what the function response will be. Thus giving the developer a simple and predictable way of setting up and controlling the state of the given mock object. For example in a restaurant online ordering application, before creating an order, it is necessary to check that all the ingredients are available, so we would mock the stock keeping service, by setting up whether each ingredient is in stock.

Example of setting up current state

Assertion Stage

In the assertion phase of the test, it is possible to query the mock to ensure that the expected functions were called with the correct parameters. This gives us the confidence to know that the code is interacting with its collaborators in the expected manner. For example in the online ordering application, we can assert that the order was saved to the database.

Example of asserting the mock was called correctly

Mocks allow for writing tests that only execute the behaviour of the component under test, without executing or depending on the behaviour of other unrelated components.

Some of the reasons to use mocks are:

  • The function under test returns or modifies data from an external source, for example a database or other microservice. Setting up this external state is often very complex and non-deterministic due to the possibilities of network outages, and results in slow running tests that need to connect to external network resources. It is also usually undesirable to require adding and deleting data in the real database in order to test the functionality.
  • Testing “rainy day” scenarios, for example simulating a network error when trying to save to the database.
Example of testing “rainy day” scenarios
  • The collaborator returns non-deterministic values, for example current time. For example the online ordering service should not allow new orders to be placed after 10:00 pm. It is necessary to be able to “take control” of the time, in order to test this scenario.
Example of testing with non-deterministic collaborator
  • The test requires a complex set up of the internal state and behaviour of collaborators, mocking those collaborators gives more control and simplifies the ability to set up the required state. It also removes the requirement of knowing the internal implementation details of the collaborator, in order to get the collaborator into the required state for the test. For example, collaborator.isValid — needs to have knowledge of the implementation of isValid, in order for the test to set up the collaborator to return true. When that logic changes, the tests need to change too, to reflect this new logic. If a mock is used, it is simple to tell the mock to return the required value.
Example of testing complex internal setup of collaborator
  • The collaborator doesn’t exist yet. During the TDD development process, it is common practice to develop the system using a layered approach, from the outside inwards. So it is necessary to mock the inner layers until they are actually developed. Using this approach it is possible to defer decisions about the internal implementation details, for example which database to use, while allowing the development of the outer layers to progress.

In this first part of the series, I have defined what mocking is, and described some of the use cases where it is needed in order to make tests clearer and easier to write. Be sure to read the next part, where I take a look at some of the strategies used in the Server Guild at Wix, and compare some of the mocking libraries that we use.

--

--