Writing tests with confidence — React, GraphQL, Typescript and MSW
While writing tests, it is often necessary to ensure that tests we write depict the actual user interaction. That is, what the user sees and how he/she interacts with it rather than testing implementation details. Testing implementation details or mocking various parts of the logic in unit tests often lack confidence and provides a false positive in many scenarios.
A funny tweet depicting the problems with unit tests
In GraphQL, Apollo has the MockedProvider component that one can use for writing tests. The problem with using MockedProvider is that it is different from the actual ApolloProvider and, matching the queries with mocked data needs quite an amount of work to set up. That is, testing scenarios with different kinds of variables with mocked queries or with different HTTP status codes is often very difficult.
Writing mocked queries is also an implementation detail testing. Hence, a different approach to writing tests is needed.
Mock Service Worker integration
MSW is a library that provides APIs to mock HTTP requests at the network level without the need to create a server.
The library can be used with both GraphQL and REST API and can be used for two different use cases as follows:
- Mock backend APIs at the network level in the browser using service workers during development when the API is not ready
- Test the queries in the Node environment without having to use MockedProvider and mocked queries.
The following video is from msw’s official website that describes how service workers are used to intercepting network requests in the browser.
However, the library uses node-request-interceptor library to intercept the network requests in the Node environment where service worker is not available.
With all this in place, we can write tests that assert how the application reacts when a network request is fired without having to worry about how it is built(aka implementation details)
Demo
I have used GitHub GraphQL API for integration and, you can view the complete code here.
Our simple application looks like the below that lists the details of two repositories and provides the ability to add and remove stars for the selected repository.
Install dependencies
At first, we need to install the following dependencies. You can view the package.json in the project
- MSW for intercepting network requests
- cross-fetch is a polyfill for fetch to use in node environments. There are also popular libraries that provide fetch polyfill such as whatwg-fetch, node-fetch etc.,
- @testing-library/react
- Apollo client and React related dependencies.
- react-scripts for setup configurations
Create Components
Let’s create App and Repository components as provided in the links.
Implementation
Let’s move the Apollo client object to a separate file so that it can be used for both testing and production code. Here we use cross-fetch as a polyfill for fetch in node environment only when the NODE_ENV is test
Writing Tests
First, we need to create server.ts file as below which imports setupServer from “msw/node”
The corresponding GraphQL handlers should be created as follows. Here we intercept the GraphQL query and send custom responses based on the query variables. Here, we import graphql from msw and, we use the methods graphql.query and graphql.mutation for query and mutation respectively.
In the above code, we can send as many custom responses easily as per query variables. The tests do not require any additional setup which, we will see in the next step.
Our final App.test.tsx will look like the below code.
Now, we don’t have any additional setup or MockedProvider. Here, we use ApolloProvider directly and the actual client object as seen in the snippet.
In the tests, we have assertions like how the user actually would interact with the application without any code for implementation details.
In the above code, I have used a function setupServer from the test-utils folder. It is extracted to a separate function so that we could use this in tests that need msw.
This function creates listeners to msw handlers and also cleans up the code after running the test suite.
It is important to include the below statement in the beforeAll. This helps to throw warnings if the components send requests to any unhandled queries.
server.listen({onUnhandledRequest: “warn”})
Another useful function is server.use which could be used to create different handlers for individual tests
And we have successfully created tests using MSW.
Mocking API in the browser
If you are interested to mock the backend API and use them in the browser, you need to call setupWorker function from msw.
We should create mockServiceWorker.js using the docs
Conclusion
We should always write tests that are implementation agnostic. It helps us avoid refactoring tests when we change the implementation and reduce the burden of context switches.
In recent years, the web world has evolved and, the way we write tests has changed. Thanks to libraries like @testing-library/react that brought different mind shift to writing tests.
We should always try to avoid the false positives that come from tests and should understand when and how to write tests.