Auto-generated mocks using Mockshot and Jest Snapshot

Shlomo Kraus
Iqoqo Engineering
Published in
9 min readMar 11, 2019

We’ve been using Mockshot extensively in-house for over a year and recently released it to the public domain. See how it takes away the pain of generating and updating mocks and how it increases the quality of your coverage.

Writing and maintaining mocks is tedious. Instead of doing it manually, you can automatically generate them during tests, using a Jest feature called Snapshots. Mockshot leverages the “assert” phase of a test and generates an accurate representation of the object under test. Just like that, you have automatic mock creation and a guarantee that the data is always valid.

This blog will show you how to quickly integrate the library into your existing tests, but if you are impatient, jump over to our repo:

Background

Shifting responsibility for mocks

Given we have two services, one of them is a provider and the other is a consumer (who consumes the provider). Usually, when testing the consumer, you will set up a mock of the provider and inject it into the consumer.

This essentially means — the consumer is the one responsible for the mocks it requires. That pattern is problematic since the provider may change without the consumer knowing it. It also forces the person writing the test to have knowledge of the provider’s inner mechanics.

A better approach would be shifting the ownership to the provider — the provider will publish its mocks and take the responsibility for keeping them updated. That way, they are accurate and ready for use by any consumer.

The best time to achieve this is while testing the provider: A provider creates a mock using its own shape and data, then assert the results against it. If a method’s output changes, the assertion will fail and require you to update the mock.

When the mock is updated, the change will be pushed to any consumer using that mock, forcing them to update (if necessary).

Coupling tests

We refer to this method as Test Coupling (or Chaining) since it couples the tests together with shared mocks. This allows your unit tests to run in isolation, but yield the results of an integration test.

Mockshot gives you this feature out-of-the-box while saving you hours of manual creation and updating of mocks. If you are using Jest, you can start using Mockshot right now since it uses the Snapshot mechanism of Jest.

I will show you how easy it is to integrate Mockshot into your current tests, but first let’s make sure we all know what Snapshot Testing is all about.

Quick introduction to Snapshot Testing

(If you are already familiar with Jest Snapshots, you should skip this section)

Jest has a snapshot mechanism for cases where the assertion is done on complex objects, ones that are hard to create manually. For example, to test the result of html rendering without snapshots, you would have to manually render it, save the (long) result into a file, and then load it with every test.

Snapshot testing makes this process easier, let’s see how it works:

it("Should render the markup", () => {const htmlResult = render() // === "<div>hello world</div>";expect(htmlResult).toMatchSnapshot();
});

When running this test, the following will happen:

  1. On the first run, jest will create a component.spec.ts.snap file inside __snapshots__ directory with the content of htmlResult
  2. On the second run — jest will load the content of that file and assert the runtime value against it. If it had changed, the test will fail.
  3. If the update was intentional, the user will tell Jest to update the snapshot. At that point, Jest will recreate the file from step 1, and the test will be green again.
Jest test fails due to snapshot mismatch (taken from Jest’s API documentation)

Mockshot in Action

Now that we know how Jest uses snapshots and what test coupling is all about, let’s see how to add Mockshot to any test. We will use a sample project and see the three steps needed:

  1. Creating mocks during the test
  2. Generating code with Mockshot CLI
  3. Consuming the mock

All the code is available here:

1. Creating mocks

Our repository is set up with Jest and Typescript. You don’t have to use Typescript, but you will soon see how autocompletion makes it easier to work with generated mocks.

Introducing our actors

We have 2 classes in our project: UserService which creates users in database and MessageService which sends email to users based on id.

Let’s start with UserService, it has one method that simply fetches a user from a database and parses the result:

Writing the test

Let’s write the test for getUser. For the sake of this example, we create a fake db service that always returns the same values. Depending on your preferences, you can also use a live database but it is up to you. Please note that this is a very simplified example.

Running the test will yield a positive result:

Asserting shape to prepare mocks

Our test checks the values of the result object, but it doesn’t watch the shape. For example, if the service will also return a password field, we wouldn’t know about it.

Also, if we have many fields and nested fields in the user object, the assertion will be very long and cumbersome. Let’s use a snapshot to assert the shape, generating a mock along the way for later use.

  1. First, install Mockshot:
yarn add mockshot --dev// ornpm install mockshot --save-dev

2. Add the following lines to the test:

import "mockshot"...it("Should get user", async () => {...expect(user).toMatchMock("UserService", "getUser", "success");})

This tells Mockshot to generate a mock for the method getUser in UserService class and name this mock success. Every method can output as many mocks as needed to represent the different values it returns.

Now when we run the test we see that Jest outputs “1 snapshot written”:

However, since you used Mockshot to emit this snapshot, it is serialized in a special format and with annotations that enable us to serialize it back and use it as mocks.

Mockshot is writing a snapshot

2. Generating mocks using CLI

The snapshots we generated are blueprints for any future mocks. They contain the object’s data, along with associated metadata, but one more step is required to consume them.

Mockshot comes with a CLI script that does exactly that, from the root folder of the project run:

$ yarn run mockshot
✨ Done in 2.18s.

Now take a look at the folder structure. You can see we have a “@mocks” directory added to our root folder. This folder has many interesting things in it, but for now, let’s focus on our UserService:

Our newly created folder “@mocks”

Inside “@mocks” you can see a folder with the name of our project — “mockshot-example” and within it, there are two files, one Typescript, and one vanilla javascript. You can use either of them, the difference is that Typescript will also give you type-safety and autocomplete.

Take a look inside @mocks/mockshot-example/UserService.ts:

This code was generated by Mockshot based on the snapshot we created earlier. You can see it has UserServiceMocks class with a single method called getUser, the name we passed to toMatchMock.

This method accepts a conditional string value which currently only holds a single value success(since this is the only one we created) and returns an object which holds the same value as in our assert.

Before we investigate this any further, let’s see how we use the mock.

3. Consuming mocks

To demonstrate how we use that mock, let’s write another class, MessageService. This service is using UserServicethat is passed in the constructor and uses the result of getUserto prepare a message object.

Please note: this is a bad design since it couples our two services with dependency. It is only used to demonstration purposes. In real world you would obviously have this service accepts “email” and “name” instead of requiring it to fetch them.

And the test for this service:

The important lines are 2 and 6–9.

First, we import the file from the folder that was created earlier:

import { UserServiceMocks } from "../@mocks/mockshot-example/UserService";

Then we can use the mock:

const getUserResult = UserServiceMocks.getUser(“success”);

By looking at the file generated by Mockshot, we know that the return value of UserServiceMocks.getUserwill be:

{
"email": "test@iqoqo.co",
"id": "abc",
"name": "Some Name"
}

That’s it!

One method, multiple mocks

We named our mock value success since it represents the value we receive on a successful operation. However, a method may have multiple different values. For example, getUsersmight return a list on success and empty array on no results.

By setting different names we can create mocks for different outcomes:

.toMatchMock(UserService, "getUser", "success");
.toMatchMock(UserService, "getUser", "no-user");
.toMatchMock(UserService, "getUser", "failed");

Now, we can see how Typescript really come at hand:

We have autocomplete for our mock methods and types! That means the consumer of UserService can write tests without having to deal with the actual service. Again, this comes out-of-the-box with Mockshot — no need for any extra setup.

Wrapping up

  • By shifting the responsibility of generating mocks from the consumer to the provider we can achieve more quality using the same coverage.
  • In our project, we had 2 classes — UserService which is the provider and MessageService which is the consumer of User.
  • We saw that by integrating Mockshot toMatchMock method during the test, we created a functional and accurate mock of UserService.getUser
  • We saw how Mockshot CLI uses the artifacts of the test to generate code and put it in @mock directory, available for everyone.
  • Consuming the mock is done by simple import, with autocomplete and intelli-sense available out of the box.

What else can Mockshot do?

This was just an introduction. We developed Mockshot for our own use and continuously update the library with every feature needed to help our tests run smoothly.

Here is some stuff that was not covered:

  1. Ignore fields — if you have a field that changes between tests, for example, an ID field in database creation, your tests will never match the Snapshot. By specifying the fourth argument, you have a way to tell Jest only to check that a field exists and is the correct type without examining the actual value.
  2. API Response — we only saw how to write tests for classes, but when testing API endpoints there is other data we want to collect: status code, headers, etc. Mockshot comes with toMatchApiMockmethod that accepts a vanilla node Response object and serializes it correctly.
  3. API Consumers — The other pillar of the previous section is how to consume an API response since it is not divided into classes and methods, rather it is based on HTTP methods and paths. That’s why Mockshot CLI will generate API mocks with a different format, consumed with API.get("/path/to/resource").success. The consumer doesn’t need to know the class and method that create the response, only the path, and method.

--

--

Shlomo Kraus
Iqoqo Engineering

IQOQO Co-Founder, VP R&D. Creator of “Stayed Up All Night” (suan.fm). Writer for Haaretz on culture, arts, and nightlife.