Making integration tests easier with MSW
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.
Subscribe to my newsletter
Read articles from Jonathan Galdino directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Jonathan Galdino
Jonathan Galdino
๐ง๐ท Software Engineer | I usually write about backend stuffs. I'm looking for a job, so please, if you're interest, hit me up.