I want a mock API and change responses on runtime

17 November 2021mockingsoftware development

Nowadays many web applications are Single Page Apps that connect to an API via HTTP (for example REST or GraphQL). When you develop such an application, you do not only have to run a local development server, but also an API where it connects to. The API could be running on a dev cluster, but you can also run it locally on a different port for example.

A big advantage of connecting to a real API, is that you probably create less bugs. On the other hand, it is sometimes hard to test or setup up all possible scenarios. How do you for example test, in a controlled way, that your API responds with an internal server error and how the client handles this? In this blog I explain how to do this.

Why you want a mock API

In this case you want to mock the API. Mocking an API is in essence a simple concept. You just replace the real implementation, that probably involves a lot of business logic, with a simple static response. Technically this can be done in several ways. You could implement a web server from scratch, that returns different static responses per endpoint. Or use one of the many libraries that can generate mock data or even a fully fledged web server for you. It is just coding the real web server, but you just take some shortcuts.

The actual problem of mocking: having dynamic responses

When this is done, there is still one problem: the mock responses are still static. They never change, unless you change the code. When you call GET /users you either get a list of mocked users or you get a HTTP 401 status code. Whatever you implemented to return. You cannot switch between responses without changing the code for your mock server. At first sight this might not be a big problem, but there could be many variants of responses that your client handles differently. In case of the /users endpoint this could be a list of users, an empty list of users, 401 Unauthorized or 500 Internal Server Error. You don't want to comment and uncomment blocks of code to change the mock responses for an endpoint.

Changing API mock responses with ease

To switch mock responses on runtime without having to change code and reloading the server, I created MSW HTTP dynamic middleware. This is a small extension of MSW HTTP middleware, a piece of Express middleware for generating API endpoints that makes use of MSW handlers.

That was a mouthful of technologies, so let me give a a quick example of how to use it.

First you have to create a new web server in ExpressJS and use the middleware.

const express = require('express');
const { createMiddleware } = require('@mswjs/http-middleware');
const { middleware } = require('msw-dynamic-http-middleware');
const handlers = require('./handlers');

const app = express();

app.use(express.json());
app.use(createMiddleware(...handlers));
app.use('/ui', middleware);

const port = 9800;
app.listen(port, () => console.info(`MSW server running at http://localhost:${port}`));

Line 9 and 10 are definitely the most interesting.

First you are passing an object with handlers to the MSW HTTP middleware that creates mock endpoints for you. This object can be generated by calling createHandlers with scenarios you defined. This is just an object with scenario names as key and (and) MSW handler(s) as value, see for instance:

const { rest } = require('msw');
const { createHandlers } = require('msw-dynamic-http-middleware');

const scenarios = {
  // Scenarios for one endpoint
  'user success': rest.get('/user', (req, res, ctx) => res(ctx.json({ name: 'frank' }))),
  'user error': rest.get('/user', (req, res, ctx) => res(ctx.status(500))),
  'users success': rest.get('/users', (req, res, ctx) => res(ctx.json([{ name: 'frank' }]))),
  'users error': rest.get('/users', (req, res, ctx) => res(ctx.status(500))),

  // Scenarios for multiple endpoints
  success: [
    rest.get('/user', (req, res, ctx) => res(ctx.json({ name: 'frank' }))),
    rest.get('/users', (req, res, ctx) => res(ctx.json([{ name: 'frank' }]))),
  ],
  error: [
    rest.get('/user', (req, res, ctx) => res(ctx.status(500))),
    rest.get('/users', (req, res, ctx) => res(ctx.status(500))),
  ],
};

// Create handlers for the scenarios and set 'success' scenario when server starts
const handlers = createHandlers(scenarios, 'success');

module.exports = handlers;

Additionally one extra endpoint is created. If you visit http://localhost:9800/ui a UI is served showing you controls to change mocks per endpoint. There are even presets to set a group of mock handlers in one click!

Change mock handlers per endpoint using the UI

Connect your client with the mock server

Now you're having a mock server running on port 9800, you need to proxy the API calls from you local client - that for example runs on port 3000 - to this server.

How you do this, differs per dev server. Here are some links to the docs for some modern frameworks:

How to change mock responses programmatically?

The UI is very convenient when you are developing the client. You test/implement the scenarios manually so you change mock handlers when needed.

In end-to-end tests however, everything is automated and there is no user interaction. How can we test the success scenario and then the error scenario?

Fortunately there is and extra /scenario endpoint generated that can be called during the test.

EndpointWhen to use
GET /scenarioGet all loaded scenarios and activated handlers
PUT /scenarioActivate a scenario
DELETE /scenarioReset mock server to initial state, optionally with ?resetAll=true to also deactive default scenario handlers

Create a reusable E2E util

You probably have to call the /scenario endpoint a lot of times in you test suites. Furthermore you don't want to pollute the test log with the endpoint calls. If you are using Cypress, I advice to create reusable Cypress commands. In other testing frameworks this can be done in a similar way.

Cypress.Commands.add('setScenario', { prevSubject: false }, scenarioName => {
  return cy
    .log(`Set scenario "${scenarioName}"`)
    .request({ method: 'PUT', url: 'http://localhost:9800/scenario', body: { scenario: scenarioName }, log: false });
});

Cypress.Commands.add('resetScenarios', { prevSubject: false }, resetAll => {
  return cy.log('Reset MSW Server').request({
    method: 'DELETE',
    url: `http://localhost:9800/scenario${resetAll ? '?resetAll=true' : ''}`,
    log: false,
  });
});

To prevent test state leakage in between tests, you also have to reset the mock server before every test.

Your tests will look like this:

beforeEach(() => {
  cy.resetScenarios();
});

it('should get the users successfully', () => {
  cy.setScenario('users success'); // Not needed when the user success scenario is already loaded on startup / after resetting.

  cy.visit('/users');
  cy.contains('John Doe').should('be.visible');
});

it('should show error when users cannot be fetched', () => {
  cy.setScenario('users error');

  cy.visit('/users');
  cy.contains('Oops! Something went wrong').should('be.visible');
});

More usages of this library

I have covered local development and end-to-end testing, but there are two other situations where you want to mock you APIs:

  1. Unit / integration tests
  2. Storybook

Using scenarios in unit tests

Remember that a unit test is both automated and it runs very quickly. Like with end-to-end tests the user cannot set mock responses in between tests and there is also no time to call a real HTTP endpoint. You have to mock HTTP traffic.

Kent C. Dodds wrote an excellent article on why you should not mock fetch (or Angular's HttpClient for that matter).

If you follow his advice by using MSW, you are mocking endpoints in a test by calling mockServer.use(someMswHandler). Those MSW handlers can quite complex/large.

Furthermore when you test a complex component that calls multiple APIs, you have to set a list of mock handlers. This affects the readability of the tests and you want to abstract this away.

This is where scenarios come in handy. Just import them and pass them to the mockServer.

import { success } from '../scenarios';
mockServer.use(...success);

This is just code, so it means that these scenarios can be shared among your unit tests and local mock server.

Using scenarios in Storybook

In Storybook you mainly develop dumb components. These are components that do not have (a lot of) dependencies and side effects such as APIs, global state or a router.

In case you still want to create a story of a component that calls an API, you have to mock the API the same way as you do in your development server.

This means that you have to proxy API calls to the mock server from within Storybook using a decorator.

Since this is a topic on its own, I will elaborate on this in a future blog post.

Conclusion

Mocking APIs during development or testing can be a lot of work or frustration. Changing the mock responses manually or programmatically without changing a line of code is not even possible with the solutions that are currently available, as far as I know.

Fortunately MSW HTTP Dynamic Middleware is a tool that solves this problem and smoothly integrates into your current development workflow.

Clone the repo and run the example project to see it in action.

I also hosted the demo for you.

  1. Call the GET /users endpoint to get a list of users with status 200
  2. Click the ‘users error’ button
  3. Call the GET /users endpoint again to get status 500