Integration tests with MSW

3 mins |

November 16, 2020

I prefer writing more integration tests than unit tests. I talk about that in Testing: Building application with confidence. While writing integration tests, you might have a nested child component that is making some network API calls. And that becomes bit difficult to mock when you don’t know/care which component is making API calls.

There isn’t an easy way to know if there are any APIs that are mocked or not.

1import axios from "axios"
2
3export const USER_API_ENDPOINT = "/api/v1/user"
4export const DETAIL_API_ENDPOINT = "/api/v1/detail"
5
6export function fetchUser() {
7 return axios.get(API_ENDPOINT)
8}
9
10export function fetchDetail() {
11 return axios.get(DETAIL_API_ENDPOINT)
12}

Let’s say we have these API calls somewhere in our application. Even after we identify that we have two API calls made from some nested child component. Mocking them isn’t that easy either.

We either have to find an axios mocking library or do some magic 🔮 to get all these mocks to work. We might have to do something like this

1jest.mock("axios")
2
3axios.get.mockImplementation(url => {
4 switch (url) {
5 case "/api/v1/user":
6 return Promise.resolve({ result: { user: "foo" } })
7 case "/api/v1/detail":
8 return Promise.resolve({ result: { detail: "bar" } })
9 default:
10 return Promise.reject(new Error(`${url} doesn't have a mock defined`))
11 }
12})
13
14test("should successfully fetch user", () => {
15 // test assertions
16})
17
18test("should successfully fetch details", () => {
19 // test assertions
20})
21
22test("should fail fetch user", () => {
23 // test assertions
24})

This works fine, but now how would you change the mocks for a particular test case. At this point, we could use libraries like nock. But it needs additional adapters if we are using libraries like axios.

Even after going through all of this, when we have to update libraries, switch from axios to fetch or to any new shiny library, We would have to update/rewrite all these tests.

And another major problem I have faced is when we use any third-party SDKs. And they make API calls. We can’t mock them reliably. Also, it is difficult to detect the API calls it is making.

So the problems I was facing are:

  1. Detecting all the API calls made.
  2. Handling different mock results for a particular test case.
  3. Changing mock for a particular test case.
  4. Handling third-party APIs (or SDKs)
  5. Tests were fragile for refactoring to a new or upgrading library

MSW to the rescue

Need not be said, I wasn’t happy with the tests we were writing. During this time I came across the Mock Service Worker library. It was officially recommended by React Testing library for mocking APIs.

So I fiddled around MSW for some time to see if it solves my problem. And it did solve the problems I listed above.

I realised within a few minutes of trying out MSW, that I was mocking networks calls. And earlier I was just stubbing/monkey patching implementation of network request making libraries.

This is amazing 🕺, I don’t know why we didn’t have MSW. And why wasn’t it a thing until recently?

1. Detecting all the API calls made.

I have this running in setupFilesAfterEnv of jest

1// test-utils
2import { setupServer } from "msw/node"
3
4export const server = setupServer()
5
6// setup-after-env
7import { server } from "test-utils"
8beforeAll(() => {
9 server.listen({
10 onUnhandledRequest: "warn",
11 })
12})
13
14afterEach(() => {
15 server.resetHandlers()
16})
17
18afterAll(() => {
19 server.close()
20})

So onUnhandledRequest in server.listen helps me in detecting API calls those are not mocked.

2. Handling different mock results each one of them.

We can attach handlers to MSW “server” to respond to the API endpoints. Let’s update those tests we wrote in the beginning, and see how it looks now.

1import { server } from "test-utils"
2
3beforeEach(() => {
4 server.use(
5 rest.get("/api/v1/user", (req, res, ctx) => {
6 return res(ctx.json({ result: { user: "foo" } }))
7 }),
8 rest.get("/api/v1/detail", (req, res, ctx) => {
9 return res(ctx.json({ result: { detail: "bar" } }))
10 })
11 )
12})
13
14test("should successfully fetch user", () => {
15 // test assertions
16})
17
18test("should successfully fetch details", () => {
19 // test assertions
20})

3. Changing mock for a particular test case.

We can use server.use to update the API response in each test cases. If we want this to apply only once, we can also do res.once.

1import { server } from "test-utils"
2
3beforeEach(() => {
4 server.use(
5 rest.get("/api/v1/user", (req, res, ctx) => {
6 return res(ctx.json({ result: { user: "foo" } }))
7 }),
8 rest.get("/api/v1/detail", (req, res, ctx) => {
9 return res(ctx.json({ result: { detail: "bar" } }))
10 })
11 )
12})
13
14test("should successfully fetch user", () => {
15 // test assertions
16})
17
18test("should successfully fetch details", () => {
19 // test assertions
20})
21
22test("should fail fetch user", () => {
23 server.use(
24 rest.get("/api/v1/user", (req, res, ctx) => {
25 return res.once(
26 ctx.status(500),
27 ctx.json({ message: "Internal server error" })
28 )
29 })
30 )
31 // test assertions
32})

4. Handling thirdparty APIs (or SDKs)

When it comes to handling API calls for third-party APIs, it is exactly same as we would do for our APIs.

1server.use(
2 rest.get("https://www.somethirdpartyapi.com/api/foo", (req, res, ctx) => {
3 return res(ctx.json({ result: "Third party response" }))
4 })
5)

But sometimes, we would be using third-party SDKs. In such cases, the endpoint used to make API call is an implementation detail of the SDK. We shouldn’t be testing based on that. So we could do something like

1import { server } from "test-utils"
2import { sdk } from "./handlers/sdk"
3
4server.use(
5 sdk("ListResources", (req, res, ctx) => {
6 return res(ctx.json({ id: 1 }))
7 })
8)

Look at Custom request handler more details.

5. Tests were fragile for refactoring to a new or upgrading library

So let’s remove axios and use fetch instead. And let’s see what we need to update in our tests.

1-import axios from "axios"
2+async function fetchGet(url) {
3+ const response = await fetch(url);
4+ const json = await response.json();
5+ return json;
6+}
7
8export const USER_API_ENDPOINT = "/api/v1/user"
9export const DETAIL_API_ENDPOINT = "/api/v1/detail"
10
11export function fetchUser() {
12- return axios.get(API_ENDPOINT)
13+ return fetchGet(API_ENDPOINT)
14}
15
16export function fetchDetail() {
17- return axios.get(DETAIL_API_ENDPOINT)
18+ return fetchGet(DETAIL_API_ENDPOINT)
19}

You know what, we don’t have to change a single character in our tests. This is awesome 🥳!!!.

Conclusion

Mock Service Worker solves most of the problems I have with mocking APIs while testing. It need not be just REST APIs, it can do graphql requests too.

All those handlers we wrote for testing can be used during development too.

Something I miss from mocking fetch or using nock, is the ability to assert on the request params (such as search params or body). When we mock something, we are creating holes in the application. To patch those holes, we would need some kind of assertions to make sure our mocks are not tearing apart the reality. For eg. When we mock functions, we do check if they were called X times, what params was it called with. There are ways to handle it in msw, but I’m not very happy about the solutions.

We didn’t have to know about what libraries were used to make API calls, we didn’t have to change anything in our code to make API mocks work. This is possible because we are not mocking libraries, we are mocking network requests! Thank you Artem Zakharchenko for creating MSW 👏. It is awesome!


Got a Question? Bala might have the answer. Get them answered on #AskBala

Edit on Github

Subscribe Now! Letters from Bala

Subscribe to my newsletter to receive letters about some interesting patterns and views in programming, frontend, Javascript, React, testing and many more. Be the first one to know when I publish a blog.

No spam, just some good stuff! Unsubscribe at any time

Written by Balavishnu V J. Follow him on Twitter to know what he is working on. Also, his opinions, thoughts and solutions in Web Dev.