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 usersPOST /api/users
- Creates a userGET /api/users/:username
- Returns a single userPUT /api/users/:username
- Updates a userDELETE /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 inmsw
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’srest
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.