Making integration tests easier with MSW

Photo by Carlos Muza on Unsplash

Making integration tests easier with MSW

Node.js + Typescript + Jest + MSW is everything that you will need.

ยท

4 min read

Mocking HTTP endpoints can be a useful technique when performing integration tests, as it can help isolate the behavior of the system under test and make the tests more reliable and faster.

When you mock an HTTP endpoint, you replace the real endpoint with a simulated version that responds to requests with pre-defined responses. This can be useful if you want to test how your application handles different scenarios without having to interact with a real server or database.

MSW can help you achieve that and I'm here to guide you through a good way to mock your REST API endpoints.

About MSW

Why MSW?

I've been using MSW to mock REST API endpoints for a while. The setup is easy to configure, it supports both back-end and front-end applications and they also have some features to help you debug your test scenarios.

In the following code examples, I'll be using MSW to mock a node.js http environment.

Configuring it

All you have to do is create the mock server.

// mockServer.ts
import { setupServer } from 'msw/node'

export const mockServer = setupServer()

I like to start and close MSW when my tests do the same. To do that, you can use Jest's setup file and things become easier. For example:

// jest.setup.js
import { mockServer } from './mockServer'

beforeAll(() => mockServer.listen())

// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => mockServer.resetHandlers())

// Clean up after the tests are finished.
afterAll(() => mockServer.close())

With the snippet above, you tell jest to start the mock server before all tests even begin.

Then, you tell MSW to clear all handlers (a.k.a. endpoints mocked) after each test. This will help prevent any collateral mocks from affecting other tests you may create.

In the end, you tell the mock server to close after all tests.

Mocking an endpoint

I'll be using Meowfacts API as our resource provider (the API we'll be mocking).

Function to mock the endpoint

The following code is a function that will mock a specific endpoint (the endpoint that returns meow facts).

import { rest } from "msw";
import { mockServer } from "./mockServer";

interface Responses {
  [id: number]: { data: any }; // its easy to avoid using any here
}

const _defaultProps = {
  status: 200
};

const responses: Responses = {
  200: { data: ["Mother cats teach their kittens to use the litter box."] },
  400: { data: { error: "Request failed" } }
};

export const mockCatsEndpoint = ({ status } = _defaultProps) => {
  mockServer.use(
    rest.get(`https://meowfacts.herokuapp.com`, (req, res, ctx) => {
      return res(ctx.json(responses[status]), ctx.status(status));
    })
  );
};

As you can see, we can build defined responses for each status. Usually, a good API has documentation with the statuses and their payload schema. You don't need to create all of them. Create just what your code uses.

P.S.: you can modify and customize this function to even receive the data that goes into the response. Enjoy modifying it for your purposes.

Examples of tests

Let's create two tests. Both will assert exactly what comes from the request: body and status.

import axios from "axios";
import { mockCatsEndpoint } from "./mockCatsEndpoint";

interface CatsResponse {
  data: string[];
}

describe("Main test", () => {
  it("should return an one meow fact", async () => {
    mockCatsEndpoint();

    const {
      data: { data }
    } = await axios.get<CatsResponse>(
      "https://meowfacts.herokuapp.com/?count=1"
    );

    expect(data.length).toBe(1);
  });

  it("should return exactly what was mocked", async () => {
    mockCatsEndpoint();

    const {
      data: { data }
    } = await axios.get<CatsResponse>(
      "https://meowfacts.herokuapp.com/?count=1"
    );

    expect(data).toEqual([
      "Mother cats teach their kittens to use the litter box."
    ]);
  });

  it("should return status 200", async () => {
    mockCatsEndpoint();

    const { status } = await axios.get<CatsResponse>(
      "https://meowfacts.herokuapp.com/?count=1"
    );

    expect(status).toEqual(200);
  });
});

With our reusable function, we can call it on each it block and mock the endpoint we need.

You can also use beforeEach and avoid duplicate lines of code :)

How can I be so sure that MSW works?

You can try mocking for several statuses: a bad request (400) should work.

P.S.: Axios throws an error when a response with status falls out of the 2xx scope.

import axios from "axios";
import { mockCatsEndpoint } from "./mockCatsEndpoint";

interface CatsResponse {
  data: string[];
}

describe("Main test", () => {
  // other tests
  it("should return status 400", async () => {
    mockCatsEndpoint({ status: 400 });

    expect(
      axios.get<CatsResponse>("https://meowfacts.herokuapp.com/?count=1")
    ).rejects.toThrow();
  });
});

That's it.

You can extend the way you create functions to mock endpoints. I like to create this way because it improves the readability of my code. It tells other developers EXACTLY what is being mocked.

I also publish a git repository with all the code. Feel free to take a look.