React Testing Librbary & Jest part 8 Handling Data Fetching in Tests === ![](https://i.imgur.com/GYOSSUa.png) --- ###### tags: `React Testing Library`, `Jest`, `React` ## We've got a bug. Let's say we've received a bug ticket saying that homepage should display popular repos of python and Java, but now it only displayed like below. ![](https://hackmd.io/_uploads/HkoKB7D43.png) ### βœ… Analyze it. Open dev tools and click `components`, we can see that each table was generated from `RepositoriesTable` components. ![](https://hackmd.io/_uploads/BkRuUXvV2.png) All tables were from `HomeRoute` component, so we can open vscode and visit `HomeRoute` component to see more info. ![](https://hackmd.io/_uploads/H1hR8XPNn.png) Seemed like there's a hook called `useRepositories` and we need to pass some sort of info as params, let's try to add `python` and `Java` here. ![](https://hackmd.io/_uploads/Bk4zPQDNh.png) Try to replicate the same pattern as other languages, and indeed we've got `python` and `Java` tables after saving file. ![](https://hackmd.io/_uploads/Hyhnv7DNh.png) ![](https://hackmd.io/_uploads/SJbZ_mP4n.png) ### βœ… How to test data fetching? **Data Fetching in Tests** - Don't make actual network requests. - It gets slow and data might change. - Fake / mock data fetching in test. **Options for Data Fetching** - Mock the file that contains the data fetching code. - Use a library to 'mock' axios = get axios to rturn fake data. - Create a manual mock for axios. #### βœ’οΈ Mock the file that contains the data fetching code In `HomeRoute` component, we understand that `useRepositories()` is the hook that we can pass params and it will return data property, therefore, we can create a **module mocking** to simulate the hook that return some sort of data. ```javascript jest.mock('../hooks/useRepositories', () => { return () => { return { data: [ { name: 'react' }, { name: 'bootstrap' }, { name: 'javascript' } ] } } }) ``` ##### πŸ“ˆpros: - Easy to test. ##### πŸ“‰cons: - We won't be able to know if there's any interaction between hook and the component. - Not sure if we are using the hook correctly. --- #### βœ’οΈ Use a library to 'mock' axios = get axios to rturn fake data Since we don't want to fetch data in test, we can use library like msw ([Mock Service Worker](https://mswjs.io/)), it can intercept the request and automatically respond to it. ![](https://hackmd.io/_uploads/Hy1gXNDV3.png) ##### πŸ”§ MSW setup 1. Create a file 2. Understand the exact URL, method and return value of requests that the component will make. 3. Create a MSW handler to intercept that request and return some fake data for your component to use. 4. Set up the `beforeAll`, `afterEach` and `afterAll` hook in the test file. 5. In a test, render the component and wait for an element to be visible. ##### βœ… Analyze it. Open dev tools and visit **Network**, observe the requests. It appears they are all `GET` requests and they have same prefix of `/api/repositories` with a query string. ![](https://hackmd.io/_uploads/r1mn_4vN2.png) ![](https://hackmd.io/_uploads/r1p4t4DNn.png) Then click **preview**, we will see it's an object with an `items` property, it contains tons of sub properties, but do we need all of them? Let's go back to vscode to check which component is going to receive it and render to screen. Inside `HomeRoute`, we can see that once we making the request and getting the data back from api, we make data as a props to pass down to another component `RepositoriesTable()`. ![](https://hackmd.io/_uploads/rJF2qEvEh.png) Inside of `RepositoriesTable()`, we can see we only need to retrive `id` and `full_name` from `repositories`. ![](https://hackmd.io/_uploads/S19QTVPEh.png) --- ##### βœ… Step 3 to make msw handlers ```javascript import { render, screen } from '@testing-library/react'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { MemoryRouter } from 'react-router-dom'; import HomeRoute from './HomeRoute'; const handlers = [ rest.get('/api/repositories', (req, res, ctx) => { // Here to monitor all GET request const query = req.url.searchParams.get('q'); console.log(query); // Here to mock the data after request and // we only need id and full_name return res( ctx.json({ items: [ { id: 1, full_name: 'Full Name' }, { id: 2, full_name: 'Another full name' }, ], }) ); }), ]; const server = setupServer(...handlers); // βœ… Step 4: Set up the `beforeAll`, `afterEach` and `afterAll` hook in the test file. // Hearing incoming request beforeAll(() => { server.listen(); }); // Reset to initial state afterEach(() => { server.resetHandlers(); }); // Shut down the server after test is done. afterAll(() => { server.close(); }); test('renders two links for each language', async () => { render( <MemoryRouter> <HomeRoute /> </MemoryRouter> ); }); // Add debug() to see the outcomes screen.debug(); ``` >The reason why we need to use `<MemoryRoute>` to wrap `<HomeRoute/>` is because inside the `RepositoriesTable()`, there're `<Link>` element, if we don't use `<MemoryRoute>`, we will get an error. We've seen there're `<h1>` tag with texts, but we did not see tables, that's because it's an async, we need to wait for a bit to resolve the request. ![](https://hackmd.io/_uploads/ry2MtBD4n.png) --- #### βœ… Step 5 Test code and check if elements show up. Add a `pause()` to resolve and check the result. ```javascript test('renders two links for each language', async () => { render( <MemoryRouter> <HomeRoute /> </MemoryRouter> ); // Add pause() to check await pause(); screen.debug(); }); const pause = () => new Promise((resolve) => { setTimeout(resolve, 100); }); ``` Check terminal to see the test result, we should be expected some errors. ![](https://hackmd.io/_uploads/H19oqBvE2.png) Keep scrolling and there's result after resolving the request, we can see there're two links we have made inside our test. ![](https://hackmd.io/_uploads/rkdMsSP42.png) --- #### βœ… Optimize test code. See the screenshot above, we have managed to show two links, but it's efficient to select them due to the displayed name (fullname and anotherfullname), it would be more efficient and easy to select if we give them an accessible name, i.e. to begin with javascript. ```javascript const handlers = [ rest.get('/api/repositories', (req, res, ctx) => { // Here to monitor all GET request const query = req.url.searchParams.get('q'); console.log(query); // Here to mock the data after request and // we only need id and full_name return res( ctx.json({ items: [ { id: 1, full_name: 'fullname' }, { id: 2, full_name: 'anotherfullname' }, ], }) ); }), ]; ``` When we console.log query, we can see the result like screenshot below. ![](https://hackmd.io/_uploads/B1shJd_E3.png) We can split and only return the language itself and replace the fake name to `query`. ```javascript const handlers = [ rest.get('/api/repositories', (req, res, ctx) => { // Here to monitor all GET request const language = req.url.searchParams.get('q').split('language:')[1]; // console.log(query); // Here to mock the data after request and // we only need id and full_name return res( ctx.json({ items: [ { id: 1, full_name: `${language}_one` }, { id: 2, full_name: `${language}_two` }, ], }) ); }), ]; ``` ![](https://hackmd.io/_uploads/SJGigddV2.png) We've changed `fullname` and `anotherfullname` to `javascript_one` and `javascript_two`, this would be easier and accessible to select. ![](https://hackmd.io/_uploads/r1xNG_uVn.png) --- #### βœ… Write down assertions. ```javascript test('renders two links for each language', async () => { render( <MemoryRouter> <HomeRoute /> </MemoryRouter> ); const languages = [ 'javascript', 'typescript', 'rust', 'go', 'python', 'java', ]; // Loop languages for (let language of languages) { // for each language, we see two links const links = await screen.findAllByRole('link', { name: new RegExp(`${language}_`), }); // Assertions expect(links).toHaveLength(2); expect(links[0]).toHaveTextContent(`${language}_one`); expect(links[1]).toHaveTextContent(`${language}_tw`); expect(links[0]).toHaveAttribute('href', `/repositories/${language}_one`); expect(links[1]).toHaveAttribute('href', `/repositories/${language}_two`); } }); ``` > We usually use `findBy` or `findAllBy` when fetching data as it is async. --- #### βœ… Make fake handlers reusable. If we seperate the `handlers` function in another file and import it to wherever we need, this could be efficient, but the downside would be what if we need to add another request and return more items, therefore we need to think twice as how to make our `handlers` truly reusable. Common approach is to create a sort of config function. ```javascript! createServer([ { path: '/api/repositories', res:(req, res, ctx) => { return { items: [{}, {}] } } }, { path: '/api/repositories', method: 'post', res:(req, res, ctx) => { return { items: [{}, {}, {}] } } }, ]) ``` Create `createServer.js` in a folder call test under `src` directory. ```javascript! // createServer.js import { setupServer } from 'msw/node'; import { rest } from 'msw'; export function createServer(handlerConfig) { // map thr the array of config const handlers = handlerConfig.map((config) => { // Default method is get, so we don't have to define in the config return rest[config.method || 'get'](config.path, (req, res, ctx) => { return res(ctx.json(config.res(req, res, ctx))); }); }); const server = setupServer(...handlers); // Hearing incoming request beforeAll(() => { server.listen(); }); // Reset to initial state afterEach(() => { server.resetHandlers(); }); // Shut down the server after test is done. afterAll(() => { server.close(); }); } ``` Let's amend coed inside `HomeRoute.test.js` ```javascript! import { render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import HomeRoute from './HomeRoute'; import { createServer } from '../testlib/createServer'; createServer([ { path: '/api/repositories', res: (req, res, ctx) => { const language = req.url.searchParams.get('q').split('language:')[1]; return { items: [ { id: 1, full_name: `${language}_one` }, { id: 2, full_name: `${language}_two` }, ], }; }, } ]); test('renders two links for each language', async () => { render( <MemoryRouter> <HomeRoute /> </MemoryRouter> ); const languages = [ 'javascript', 'typescript', 'rust', 'go', 'python', 'java', ]; // Loop languages for (let language of languages) { // for each language, we see two links const links = await screen.findAllByRole('link', { name: new RegExp(`${language}_`), }); // Assertions expect(links).toHaveLength(2); expect(links[0]).toHaveTextContent(`${language}_one`); expect(links[1]).toHaveTextContent(`${language}_tw`); expect(links[0]).toHaveAttribute('href', `/repositories/${language}_one`); expect(links[1]).toHaveAttribute('href', `/repositories/${language}_two`); } }); ```