Integration tests with MSW
3 mins |
November 16, 2020I 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"23export const USER_API_ENDPOINT = "/api/v1/user"4export const DETAIL_API_ENDPOINT = "/api/v1/detail"56export function fetchUser() {7 return axios.get(API_ENDPOINT)8}910export 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")23axios.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})1314test("should successfully fetch user", () => {15 // test assertions16})1718test("should successfully fetch details", () => {19 // test assertions20})2122test("should fail fetch user", () => {23 // test assertions24})
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:
- Detecting all the API calls made.
- Handling different mock results for a particular test case.
- Changing mock for a particular test case.
- Handling third-party APIs (or SDKs)
- 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-utils2import { setupServer } from "msw/node"34export const server = setupServer()56// setup-after-env7import { server } from "test-utils"8beforeAll(() => {9 server.listen({10 onUnhandledRequest: "warn",11 })12})1314afterEach(() => {15 server.resetHandlers()16})1718afterAll(() => {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"23beforeEach(() => {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})1314test("should successfully fetch user", () => {15 // test assertions16})1718test("should successfully fetch details", () => {19 // test assertions20})
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"23beforeEach(() => {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})1314test("should successfully fetch user", () => {15 // test assertions16})1718test("should successfully fetch details", () => {19 // test assertions20})2122test("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 assertions32})
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"34server.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+}78export const USER_API_ENDPOINT = "/api/v1/user"9export const DETAIL_API_ENDPOINT = "/api/v1/detail"1011export function fetchUser() {12- return axios.get(API_ENDPOINT)13+ return fetchGet(API_ENDPOINT)14}1516export 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
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