An Idiots Guide to mocking HTTP with MSW

I recently had some real trouble getting MSW to work with my tests. It seems like I ran into a few gotchas that I’ll try to explain here.

For context, I’m using Vitest as my test runner and openapi-react-query to make my API calls.

MSW not matching any of my requests

After setting up MSW as per the docs

import { beforeAll, afterEach, afterAll } from 'vitest'
import { setupServer } from 'msw/node'
import createFetchClient from "openapi-fetch";
import createClient from "openapi-react-query";
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript

// Setup openapi-react-query client
const fetchClient = createFetchClient<paths>({
    baseUrl: "https://myapi.dev/v1/",
});
const $api = createClient(fetchClient);

// create a mock server and log all requests
const server = setupServer()
server.events.on('request:start', ({ request }) => {
  console.log('Outgoing:', request.method, request.url)
})

const Wrapper = ({ children }: { children: React.ReactNode }) => {
    return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
})

const Component = () => {
    const { data, error, isLoading } = $api.useQuery(
        "get",
        "/blogposts/{post_id}",
        {
        params: {
            path: { post_id: 5 },
        },
        },
    );

    if (isLoading) return <div>Loading...</div>
    if (error) return <div>Error: {error.message}</div>

    return <div>{data.title}</div>
 }


describe('MSW', () => {
    // start, clear and close the server
    beforeAll(() => server.listen())
    afterEach(() => server.resetHandlers())
    afterAll(() => server.close())

    it('should work', async () => {
        // setup a mock handler for the /blogposts/{post_id} endpoint
        server.use(
            http.get("/blogposts/{post_id}", () => {
                return HttpResponse.json({title: "My Blog Post"})
            })
        )

        await act(() => render(<Component />, {wrapper: Wrapper}))
        await waitFor(() => screen.getByText("My Blog Post"))

        // assert that the request was made
        expect(data).toBeDefined();
    });
})

If I run this test it will fail and my console.log will not be hit showing me that the request was intercepted by MSW.

After banging my head against a brick wall for a while I found the solution was that because openapi-fetch has default fetch function that somehow is not being intercepted by MSW.

Providing my own fetch implementation solved the problem.

const fetchClient = createFetchClient<paths>({
    baseUrl: "https://myapi.dev/v1/",
    fetch: (req) => fetch(req)
});

I’m not sure why this is the case, but it’s something to be aware of.

MSW not matching requests with query params

The next issue I ran into was that MSW was not matching requests with query params.

Let’s say we have a standard listing endpoint that looks like this:

server.use(
    http.get("/blogposts", () => {
        return HttpResponse.json([{title: "My Blog Post"}])
    })
)

If I try to call this with some standard pagination query params my request will not be matched. If you add query params to the path you’ll get a warning from MSW and the request will not be intercepted.

The docs don’t seem to have any special handling for query params. Turns out the docs are wrong or there is a bug in the code, you need to have a wildcard match for the query params.

server.use(
    http.get("/blogposts*", () => {
        return HttpResponse.json([{title: "My Blog Post"}])
    })
)

That’s it, hope this helps someone out there.