From f977452c5906f201840b5564701feff8247487fc Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 1 Jun 2020 08:37:50 +0200 Subject: [PATCH 1/5] docs: replace "axiosMock" with "msw" in React Testing Library example --- docs/react-testing-library/example-intro.md | 48 +++++++++++++-------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/docs/react-testing-library/example-intro.md b/docs/react-testing-library/example-intro.md index fe800acd9..054b4fafc 100644 --- a/docs/react-testing-library/example-intro.md +++ b/docs/react-testing-library/example-intro.md @@ -11,27 +11,29 @@ See the following sections for a detailed breakdown of the test ```jsx // __tests__/fetch.test.js import React from 'react' +import { rest } from 'msw' +import { setupServer } from 'msw/node' import { render, fireEvent, waitFor, screen } from '@testing-library/react' import '@testing-library/jest-dom/extend-expect' -import axiosMock from 'axios' import Fetch from '../fetch' -jest.mock('axios') +const server = setupServer( + rest.get('/greeting', (req, res, ctx) => { + return res(ctx.json({ greeting: 'hello there' })) + }) +) + +beforeAll(() => server.listen()) +afterAll(() => server.close()) test('loads and displays greeting', async () => { const url = '/greeting' render() - axiosMock.get.mockResolvedValueOnce({ - data: { greeting: 'hello there' }, - }) - fireEvent.click(screen.getByText('Load Greeting')) await waitFor(() => screen.getByRole('heading')) - expect(axiosMock.get).toHaveBeenCalledTimes(1) - expect(axiosMock.get).toHaveBeenCalledWith(url) expect(screen.getByRole('heading')).toHaveTextContent('hello there') expect(screen.getByRole('button')).toHaveAttribute('disabled') }) @@ -47,17 +49,32 @@ test('loads and displays greeting', async () => { // import dependencies import React from 'react' +// import API mocking utilities from Mock Service Worker +// (see https://github.com/mswjs/msw) +import { rest } from 'msw' +import { setupServer } from 'msw/node' + // import react-testing methods import { render, fireEvent, waitFor, screen } from '@testing-library/react' // add custom jest matchers from jest-dom import '@testing-library/jest-dom/extend-expect' -import axiosMock from 'axios' // the component to test import Fetch from '../fetch' -// https://jestjs.io/docs/en/mock-functions#mocking-modules -jest.mock('axios') +// declare which API requests to mock +const server = setupServer( + // capture "GET /greeting" requests + rest.get('/greeting', (req, res, ctx) => { + // respond using a mocked JSON body + return res(ctx.json({ greeting: 'hello there' })) + }) +) + +// establish API mocking before all tests +// and clean up once the tests are finished +beforeAll(() => server.listen()) +afterAll(() => server.close()) ``` ```jsx @@ -70,7 +87,8 @@ test('loads and displays greeting', async () => { ### Arrange -The [`render`](./api#render) method renders a React element into the DOM and returns utility functions for testing the component. +The [`render`](./api#render) method renders a React element into the DOM and +returns utility functions for testing the component. ```jsx const url = '/greeting' @@ -83,10 +101,6 @@ The [`fireEvent`](dom-testing-library/api-events.md) method allows you to fire events to simulate user actions. ```jsx -axiosMock.get.mockResolvedValueOnce({ - data: { greeting: 'hello there' }, -}) - fireEvent.click(screen.getByText('Load Greeting')) // Wait until the mocked `get` request promise resolves and @@ -133,8 +147,6 @@ export default function Fetch({ url }) { ``` ```jsx -expect(axiosMock.get).toHaveBeenCalledTimes(1) -expect(axiosMock.get).toHaveBeenCalledWith(url) expect(screen.getByRole('heading')).toHaveTextContent('hello there') expect(screen.getByRole('button')).toHaveAttribute('disabled') From 51a31d31b5d6db056a64e7e11fdf76b8b2e3760e Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 3 Jun 2020 08:33:01 +0200 Subject: [PATCH 2/5] docs: remove snapshot testing in "React Example" --- docs/react-testing-library/example-intro.md | 25 --------------------- 1 file changed, 25 deletions(-) diff --git a/docs/react-testing-library/example-intro.md b/docs/react-testing-library/example-intro.md index 054b4fafc..453a99f75 100644 --- a/docs/react-testing-library/example-intro.md +++ b/docs/react-testing-library/example-intro.md @@ -145,28 +145,3 @@ export default function Fetch({ url }) { ) } ``` - -```jsx -expect(screen.getByRole('heading')).toHaveTextContent('hello there') -expect(screen.getByRole('button')).toHaveAttribute('disabled') - -// snapshots work great with regular DOM nodes! -expect(container).toMatchInlineSnapshot(` -
-
- -

- hello there -

-
-
-`) - -// you can also use get a `DocumentFragment`, -// which is useful if you want to compare nodes across render -expect(asFragment()).toMatchSnapshot() -``` From 46415a225116715604c443f6511aec41f012b985 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 3 Jun 2020 09:05:49 +0200 Subject: [PATCH 3/5] docs: add a recommendation block for msw --- docs/react-testing-library/example-intro.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/react-testing-library/example-intro.md b/docs/react-testing-library/example-intro.md index 453a99f75..707cde7b7 100644 --- a/docs/react-testing-library/example-intro.md +++ b/docs/react-testing-library/example-intro.md @@ -39,6 +39,10 @@ test('loads and displays greeting', async () => { }) ``` +> We recommend using [Mock Service Worker](https://github.com/mswjs/msw) library +> to declaratively mock API communication in your tests instead of stubbing +> `window.fetch`, or relying on third-party adapters. + --- ## Step-By-Step @@ -50,7 +54,6 @@ test('loads and displays greeting', async () => { import React from 'react' // import API mocking utilities from Mock Service Worker -// (see https://github.com/mswjs/msw) import { rest } from 'msw' import { setupServer } from 'msw/node' From e007d842f7f54e3e4360da4a6e3289abbaef59c5 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 3 Jun 2020 09:06:20 +0200 Subject: [PATCH 4/5] docs: explain API mocking in "React Example" --- docs/react-testing-library/example-intro.md | 23 ++++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/react-testing-library/example-intro.md b/docs/react-testing-library/example-intro.md index 707cde7b7..a3322ca4d 100644 --- a/docs/react-testing-library/example-intro.md +++ b/docs/react-testing-library/example-intro.md @@ -64,7 +64,22 @@ import { render, fireEvent, waitFor, screen } from '@testing-library/react' import '@testing-library/jest-dom/extend-expect' // the component to test import Fetch from '../fetch' +``` + +```jsx +test('loads and displays greeting', async () => { + // Arrange + // Act + // Assert +}) +``` + +### Mock +Use the `setupServer` function from `msw` to mock an API request that our tested +component makes. + +```js // declare which API requests to mock const server = setupServer( // capture "GET /greeting" requests @@ -80,14 +95,6 @@ beforeAll(() => server.listen()) afterAll(() => server.close()) ``` -```jsx -test('loads and displays greeting', async () => { - // Arrange - // Act - // Assert -}) -``` - ### Arrange The [`render`](./api#render) method renders a React element into the DOM and From 62ab23801cf4ca8bdd21ce0ad1e2a1dfd0554bc1 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Thu, 4 Jun 2020 09:00:01 +0200 Subject: [PATCH 5/5] docs: add error handling scenario to "React Example" --- docs/react-testing-library/example-intro.md | 83 +++++++++++++++++---- 1 file changed, 70 insertions(+), 13 deletions(-) diff --git a/docs/react-testing-library/example-intro.md b/docs/react-testing-library/example-intro.md index a3322ca4d..a7753cbde 100644 --- a/docs/react-testing-library/example-intro.md +++ b/docs/react-testing-library/example-intro.md @@ -24,11 +24,11 @@ const server = setupServer( ) beforeAll(() => server.listen()) +afterEach(() => server.resetHandlers()) afterAll(() => server.close()) test('loads and displays greeting', async () => { - const url = '/greeting' - render() + render() fireEvent.click(screen.getByText('Load Greeting')) @@ -37,6 +37,23 @@ test('loads and displays greeting', async () => { expect(screen.getByRole('heading')).toHaveTextContent('hello there') expect(screen.getByRole('button')).toHaveAttribute('disabled') }) + +test('handlers server error', async () => { + server.use( + rest.get('/greeting', (req, res, ctx) => { + return res(ctx.status(500)) + }) + ) + + render() + + fireEvent.click(screen.getByText('Load Greeting')) + + await waitFor(() => screen.getByRole('alert')) + + expect(screen.getByRole('alert')).toHaveTextContent('Oops, failed to fetch!') + expect(screen.getByRole('button')).not.toHaveAttribute('disabled') +}) ``` > We recommend using [Mock Service Worker](https://github.com/mswjs/msw) library @@ -90,9 +107,26 @@ const server = setupServer( ) // establish API mocking before all tests -// and clean up once the tests are finished beforeAll(() => server.listen()) +// reset any request handlers that are declared as a part of our tests +// (i.e. for testing one-time error scenarios) +afterEach(() => server.resetHandlers()) +// clean up once the tests are done afterAll(() => server.close()) + +// ... + +test('handlers server error', async () => { + server.use( + // override the initial "GET /greeting" request handler + // to return a 500 Server Error + rest.get('/greeting', (req, res, ctx) => { + return res(ctx.status(500)) + }) + ) + + // ... +}) ``` ### Arrange @@ -101,8 +135,7 @@ The [`render`](./api#render) method renders a React element into the DOM and returns utility functions for testing the component. ```jsx -const url = '/greeting' -const { container, asFragment } = render() +const { container, asFragment } = render() ``` ### Act @@ -113,7 +146,7 @@ events to simulate user actions. ```jsx fireEvent.click(screen.getByText('Load Greeting')) -// Wait until the mocked `get` request promise resolves and +// wait until the `get` request promise resolves and // the component calls setState and re-renders. // `waitFor` waits until the callback doesn't throw an error @@ -131,16 +164,39 @@ fetch.js import React, { useState } from 'react' import axios from 'axios' +function greetingReducer(state, action) { + switch (action.type) { + case 'SUCCESS': { + return { + error: null, + greeting: action.greeting, + } + } + case: 'ERROR': { + error: action.error, + greeting: null + } + default: { + return state + } + } +} + export default function Fetch({ url }) { - const [greeting, setGreeting] = useState('') + const [{ error, greeting }, dispatch] = useReducer(greetingReducer) const [buttonClicked, setButtonClicked] = useState(false) const fetchGreeting = async () => { - const response = await axios.get(url) - const data = response.data - const { greeting } = data - setGreeting(greeting) - setButtonClicked(true) + axios.get(url) + .then((response) => { + const { data } = response + const { greeting } = data + dispatch({ type: 'SUCCESS', greeting }) + setButtonClicked(true) + }) + .catch((error) => { + dispatch({ type: 'ERROR' }) + }) } const buttonText = buttonClicked ? 'Ok' : 'Load Greeting' @@ -150,7 +206,8 @@ export default function Fetch({ url }) { - {greeting ?

{greeting}

: null} + {greeting &&

{greeting}

} + {error &&

Oops, failed to fetch!

} ) }