Have recently been learning how to test Reactjs components via unit testing using jest and @testing-library/react which come prebundled with any new Reactjs app created with CRA (Create-react-app CLI).
Mocking functions with jest
Many times components have eventHandlers (onclick, onchange, onblur etc) passed down to them via props and with tests these event handlers are usually mocked out.
Mocking ajax/fetch/axios calls in tests.
Since almost every app has a remote request that they make, learning to mock async calls seemed very important. For this, a package called MSW ( model service worker) can be used to intercept ajax calls to external remote sources to keep them local so that tests are less flaky and quicker.
My package.json for the example that I’m running:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
{ "name": "unit-testing-sandbox", "version": "1.0.0", "description": "", "keywords": [], "main": "src/index.js", "dependencies": { "axios": "0.21.1", "react": "17.0.0", "react-dom": "17.0.0", "react-scripts": "3.4.3" }, "devDependencies": { "typescript": "3.8.3", "@testing-library/jest-dom": "5.11.9", "@testing-library/react": "11.2.3", "@testing-library/user-event": "12.6.0", "jest-environment-jsdom-sixteen": "^1.0.3", "msw": "0.25.0", }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jest-environment-jsdom-sixteen", "eject": "react-scripts eject" }, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ] } |
Notice the inclusion of jest-enviroment-jsdom-sixteen. Further more, it’s use here:
|
|
"test": "react-scripts test --env=jest-environment-jsdom-sixteen" |
I added this to suppress some errors about not using act() during async calls. It can be further explained here: https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning
To use MSW we have to setup a server with endpoints that it will intercept. Might want to break your endpoints in to a separate file but for this example I kept them all in the same server.js under the mock directory.
|
|
// src/mocks/server.js import { rest,setupWorker } from "msw"; import { setupServer } from "msw/node"; const handlers = [ rest.get("https://jsonplaceholder.typicode.com/todos/1", (req, res, ctx) => { return res(ctx.json({ title: "the mocked title" })); }) ]; const server = setupServer(...handlers); export { server, rest }; |
What above will do is intercept any call to “https://jsonplaceholder.typicode.com/todos/1” and instead return a json object of
|
|
{title:"the mocked title"} |
Also, we want our server to setup and tear down for each and every test and for that we create a setupTests.js within the /src folder:
|
|
import "@testing-library/jest-dom/extend-expect"; import { server } from "./mocks/server"; // Establish API mocking before all tests. beforeAll(() => server.listen()); // Reset any request handlers that we may add during the tests, // so they don't affect other tests. afterEach(() => server.resetHandlers()); // Clean up after the tests are finished. afterAll(() => server.close()); |
The testing library should automatically see this setupTests.js file and run it during testing.
So the actual component we are going to test is called Todo.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
import React from "react"; import axios from "axios"; export default function Todo({ownersName}) { const [counter, setCounter] = React.useState(0); const [title, setTitle] = React.useState(false); axios.defaults.withCredentials = true; React.useEffect(() => { async function fetchData() { axios.get("https://jsonplaceholder.typicode.com/todos/1").then((res) => { setTitle(res.data.title); }).catch(e => { console.log(e.message) }); } fetchData(); }, []); return ( <div> {/** Title is retrieved on load via async call to a rest api. the h2(with the title) will only render once a value for title is set */} {title && <h2 data-testid="title">{title}</h2>} <p data-testid={"ownersName"}>{ownersName}</p> <p data-testid={"counter"}>{counter}</p> <button data-testid="increaseTodo" onClick={() => { setCounter(counter + 1); }} > Increase this Todo's counter by 1 </button> </div> ); } |
The ajax call is actually within the useEffect that gets called once on the initial rendering of the component.
Here is our testing file for Todo component. Todo.test.js
|
|
import React from "react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import Todo from "./Todo"; it("msw intercepts the axios on effect ", async () => { render(<Todo ownersName={"jason"} />); await waitFor(()=>expect(screen.getByTestId("title").textContent).toBe("the mocked title")); }); |
The actual test we are interested in is “msw intercepts the axios on effect” as this is the one that will check if the ajax call was intercept and replaced with our mocked return data that we get from MSW.
First we render the component which will trigger the intitial useEffect call which then triggers the ajax call to ultimately update our title state.
The line below enables us to “wait”(async) for the ajax call to complete and for the state to be updated before we check to see if the mocked data was in fact returned via MSW.
|
|
await waitFor(()=>expect( screen.getByTestId("title").textContent).toBe("the mocked title")); |
Our tests pass as MSW does infact return the mocked title of “the mocked title” which is what our test is looking for.
Mocking 3rd party libraries or modules
We can use jest to mock a library so our tests are easier to write without going in to the complexity of incorporating the libraries that maybe used. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
//SomeComponent.test.js import React from 'react'; import { render, screen, act } from '@testing-library/react'; import { useGetLocation } from 'react-get-location'; jest.mock('react-get-location"); // this will make // jest return a mocked function for every exported function in this module. test('it shows current location', () => { let setReturnValue; function useMockGetLocation() { const state = React.useState([]); setReturnValue = state[1]; return state[0]; } useGetLocation.mockImplemention(useMockGetLocation); }); |
We mocked the useGetLocation function that is from the ‘react-get-location’ library and made it so we return a custom state hook to be used in our tests instead of the normal non-mocked version of useGetLocation
(video lesson 184 )