Mocking the backend

End-to-end tests tend to be slow and flaky. So how about using a mock backend to test your frontend. The "mock-service-worker" library helps you create such backends. And you can use it for more than just testing.

A web-frontend rarely works without a backend, so if you want to test your frontend, the most obvious way is to start an instance of your app and run tests in an automated browser. The good thing of such end-to-end tests is that you are able to catch most bugs, no matter if the occur in backend, frontend, or due to misaligned interfaces. The downside is that such tests are often slow, flaky and use a lot of resources, because… how big is you “backend”? Does it include just a Quarkus app? Or also a database? ElasticSearch? External APIs?

If you just want to test your frontend code, there is a different way: You can prepare a mock backend for your tests. You can simulate the responses that you require. You can directly verify that certain requests have been sent to the backend, not just indirectly check what the backend responds.

But how do you do that?

nock

When I started Node.js development, I have used the library nock a lot for my tests. It hooks into the network functions of Node.js, intercepts requests and provides mock responses.

That way, your test includes the third-party libraries that you are using, like axios or got. It is built for Node.js applications and provides a nice fluent API. But since jest and jsdom run in Node.js, you can use it for frontend tests as well.

This is an example from one of my earlier projects:

const nock = require("nock");
const mockInput = nock("http://example.com")
  .get("/swagger.json")
  .reply(200, { a: "b" });

const result = await Bootprint.loadInput("http://example.com/swagger.json");
expect(mockInput.isDone()).to.be.true(); // Verify that all expected requests have been sent
expect(result).to.deep.equal({ a: "b" }); // Verify response

By default, every mock that you specify is a one-off. It will disappear after being used, so you can provide multiple responses for multiple requests.

You can also define permanent mocks, but the general approach of nock is that you give it a list of responses that you expect from the backend, for a list of requests that you expect in a specific order.

mock-service-worker

About two years ago, I came across mock-service-worker. To be honest, it does not provide as much functionality as nock, but it has a big advantage: It runs in Node.js and in the browser. In the browser, it uses the ServiceWorker-api to intercept requests and provide mock responses. You can even see the requests in the browser’s dev-tools.

This library has a programmatic approach. The mocks are functions that compute the response from the request, like a real backend. Mock-handlers are not one-off. They are kept until they are replaced by a different one.

The example above, written with msw looks like this:

const handler = rest.get(
  "http://example.com/swagger.json",
  (req, res, context) => {
    return res(context.status(200), context.json({ a: "b" }));
  },
);

Then you can use this handler in Node.js

const server = setupServer(handler);
server.listen();

or in the browser

const worker = setupWorker(handler);
worker.start();

Use cases

Why would I want to use a mock backend in the browser? All my tests run in jest (i.e. Node.js).

There are some use-cases where a mock backend makes sense to use in the browser.

  • The Gachou Demo is hosted on GitLab pages. It has no real backend, but you can still try it out. If you just want to show people, how the frontend will look like, but the backend is not ready yet, this is the way to go.
  • Integration testing with real browsers: Instead of testing your frontend with Jest, you can use e2e-tools like Cypress, but still use a mock backend.
  • Design system tools like Storybook: Are running in the browser. If you have a component that relies on certain endpoints, you mock those endpoints in your stories.

Working on my project Gachou, I implemented a simple CRUD endpoint as mock-service-worker handlers

  • GET /api/users - Lists all users
  • POST /api/users - Creates a user
  • GET /api/users/:username - Returns a single user
  • PUT /api/users/:username - Updates a user
  • DELETE /api/users/:username - Deletes a user

I used this implementation both in Jest tests and in the Gachou Demo. This makes the Jest tests feel more natural. For example if you delete a user, you can check that the correct request is sent, and you can check that the user does not appear in the list anymore, after reloading the data.

Utilities for using mock-service-worker

Sadly msw lacks some useful utilities, which I added to Gachou. I am thinking about extracting them to a library on npm - some time:

  • There is no built-in way to verify which requests have been sent to the backend. nock actually provides this, but in msw we have to implement it ourselves.
  • I wanted to wrap the user CRUD request-handlers to add a user-role-check, but only in the dev-server’s mock-backend. Wrapping request-handlers is pretty hard due to the way msw is implemented and typed.
  • I already generated the API-client from the OpenAPI-spec (see “Type-safe API clients for web-frontends”). I wanted to be able to get TypeChecks and auto-completion for the mock-api request-handlers as well, so I generated types for msw from OpenAPI as well.

I will cover some of those topics in separate articles.

So, when you read the code that implements the users mock-endpoints, be aware that

  • the oas (OpenAPI-spec) object is a wrapper around mock-service-worker’s rest object, with types generated from the OpenAPI spec,
  • and the HandlerConfig interface is part of the effort to allow wrapping endpoints with additional “middleware”-like functions.

And here it is:

Conclusion

There are at least two good libraries that allow mocking network requests for tests, but you can also use it for other stuff. Mock-service-worker is my favorite, because it also works in the browser. You have to add some code though, to get the right tools for testing.