Skip to content

docs: replace "axiosMock" with "msw" in React Testing Library example #483

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 4, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 110 additions & 56 deletions docs/react-testing-library/example-intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,55 @@ 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' }))
})
)

test('loads and displays greeting', async () => {
const url = '/greeting'
render(<Fetch url={url} />)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an afterEach(() => server.resetHandlers()) here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a safe option—yes, but unless you declare runtime handlers via server.use() there’s nothing to reset. I’d not suggest calling resetHandlers by default. Developers that need it will find it :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair point. Now that you mention it, I think the server.use call should be inside the test rather than outside it. That's what people should be doing most of the time.

With that in mind, resetHandlers should be included.

And I think it should be used by default 😉

Copy link
Contributor Author

@kettanaito kettanaito Jun 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I please double check on this?

I see your point of using resetHandlers and server.use inside a test suite, but I think I'm confused about what runtime handler to add via server.use, as the example tests exactly one API communication to "GET /greeting".

const server = setupServer(
  // centralized place for request handlers,
  // generally encouraged.
  rest.get('/greeting', resolver)
)

beforeAll(...)
afterAll(...)

test('loads and displays greeting', () => {
  // I need `afterEach(() => server.resetHandlers())`
  // only if I use `server.use()` in any of my tests.
  server.use(/* however, what should I add here? */)
})

Am I missing something in this? 🤔 Do you suggest to move rest.get('/greeting') from setupServer to server.use inside a test suite?

I'm slightly concerned that showcasing both initial (setupServer) and runtime handlers (server.use) can be overwhelming for the reader, so I'd try to keep their usage to a minimum.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fair point. There are two things to consider:

  1. Handle all of our application's typical requests so we don't have to think about them on a per-test basis
  2. Add a handler for a specific test (to test an error state)

With that in mind, I think we should adjust this example so we have two tests. One for the success case, and one for the error case. That way the first one can rely on the default handlers and one that can add a runtime handler for the error.

Because I see using afterEach(() => server.resetHandlers()) as an important default whenever someone uses server.use and we should probably demonstrate that so people don't add a bunch of complicated single-test-specific handlers to their setupServer call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is perfect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the error handling test scenario to this example.

  1. Rewrote useState to useReducer in fetch.js to reflect the error and data relation.
  2. Added a new test suite, used server.use() with permanent override there.
  3. Added afterEach that calls server.resetHanders().

Please, could you double check if that fetch.js looks okay to you? Thanks.


axiosMock.get.mockResolvedValueOnce({
data: { greeting: 'hello there' },
})
test('loads and displays greeting', async () => {
render(<Fetch url="/greeting" />)

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')
})

test('handlers server error', async () => {
server.use(
rest.get('/greeting', (req, res, ctx) => {
return res(ctx.status(500))
})
)

render(<Fetch url="/greeting" />)

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
> to declaratively mock API communication in your tests instead of stubbing
> `window.fetch`, or relying on third-party adapters.

---

## Step-By-Step
Expand All @@ -47,17 +70,17 @@ test('loads and displays greeting', async () => {
// import dependencies
import React from 'react'

// import API mocking utilities from Mock Service Worker
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')
```

```jsx
Expand All @@ -68,13 +91,51 @@ test('loads and displays greeting', async () => {
})
```

### 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
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
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

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'
const { container, asFragment } = render(<Fetch url={url} />)
const { container, asFragment } = render(<Fetch url="/greeting" />)
```

### Act
Expand All @@ -83,13 +144,9 @@ 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
// 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

Expand All @@ -107,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'
Expand All @@ -126,35 +206,9 @@ export default function Fetch({ url }) {
<button onClick={fetchGreeting} disabled={buttonClicked}>
{buttonText}
</button>
{greeting ? <h1>{greeting}</h1> : null}
{greeting && <h1>{greeting}</h1>}
{error && <p role="alert">Oops, failed to fetch!</p>}
</div>
)
}
```

```jsx
expect(axiosMock.get).toHaveBeenCalledTimes(1)
expect(axiosMock.get).toHaveBeenCalledWith(url)
expect(screen.getByRole('heading')).toHaveTextContent('hello there')
expect(screen.getByRole('button')).toHaveAttribute('disabled')

// snapshots work great with regular DOM nodes!
expect(container).toMatchInlineSnapshot(`
<div>
<div>
<button
disabled=""
>
Ok
</button>
<h1>
hello there
</h1>
</div>
</div>
`)

// you can also use get a `DocumentFragment`,
// which is useful if you want to compare nodes across render
expect(asFragment()).toMatchSnapshot()
```