@@ -11,32 +11,55 @@ See the following sections for a detailed breakdown of the test
11
11
``` jsx
12
12
// __tests__/fetch.test.js
13
13
import React from ' react'
14
+ import { rest } from ' msw'
15
+ import { setupServer } from ' msw/node'
14
16
import { render , fireEvent , waitFor , screen } from ' @testing-library/react'
15
17
import ' @testing-library/jest-dom/extend-expect'
16
- import axiosMock from ' axios'
17
18
import Fetch from ' ../fetch'
18
19
19
- jest .mock (' axios' )
20
+ const server = setupServer (
21
+ rest .get (' /greeting' , (req , res , ctx ) => {
22
+ return res (ctx .json ({ greeting: ' hello there' }))
23
+ })
24
+ )
20
25
21
- test ( ' loads and displays greeting ' , async () => {
22
- const url = ' /greeting '
23
- render ( < Fetch url = {url} / > )
26
+ beforeAll ( () => server . listen ())
27
+ afterEach (() => server . resetHandlers ())
28
+ afterAll (() => server . close () )
24
29
25
- axiosMock .get .mockResolvedValueOnce ({
26
- data: { greeting: ' hello there' },
27
- })
30
+ test (' loads and displays greeting' , async () => {
31
+ render (< Fetch url= " /greeting" / > )
28
32
29
33
fireEvent .click (screen .getByText (' Load Greeting' ))
30
34
31
35
await waitFor (() => screen .getByRole (' heading' ))
32
36
33
- expect (axiosMock .get ).toHaveBeenCalledTimes (1 )
34
- expect (axiosMock .get ).toHaveBeenCalledWith (url)
35
37
expect (screen .getByRole (' heading' )).toHaveTextContent (' hello there' )
36
38
expect (screen .getByRole (' button' )).toHaveAttribute (' disabled' )
37
39
})
40
+
41
+ test (' handlers server error' , async () => {
42
+ server .use (
43
+ rest .get (' /greeting' , (req , res , ctx ) => {
44
+ return res (ctx .status (500 ))
45
+ })
46
+ )
47
+
48
+ render (< Fetch url= " /greeting" / > )
49
+
50
+ fireEvent .click (screen .getByText (' Load Greeting' ))
51
+
52
+ await waitFor (() => screen .getByRole (' alert' ))
53
+
54
+ expect (screen .getByRole (' alert' )).toHaveTextContent (' Oops, failed to fetch!' )
55
+ expect (screen .getByRole (' button' )).not .toHaveAttribute (' disabled' )
56
+ })
38
57
```
39
58
59
+ > We recommend using [ Mock Service Worker] ( https://github.com/mswjs/msw ) library
60
+ > to declaratively mock API communication in your tests instead of stubbing
61
+ > ` window.fetch ` , or relying on third-party adapters.
62
+
40
63
---
41
64
42
65
## Step-By-Step
@@ -47,17 +70,17 @@ test('loads and displays greeting', async () => {
47
70
// import dependencies
48
71
import React from ' react'
49
72
73
+ // import API mocking utilities from Mock Service Worker
74
+ import { rest } from ' msw'
75
+ import { setupServer } from ' msw/node'
76
+
50
77
// import react-testing methods
51
78
import { render , fireEvent , waitFor , screen } from ' @testing-library/react'
52
79
53
80
// add custom jest matchers from jest-dom
54
81
import ' @testing-library/jest-dom/extend-expect'
55
- import axiosMock from ' axios'
56
82
// the component to test
57
83
import Fetch from ' ../fetch'
58
-
59
- // https://jestjs.io/docs/en/mock-functions#mocking-modules
60
- jest .mock (' axios' )
61
84
```
62
85
63
86
``` jsx
@@ -68,13 +91,51 @@ test('loads and displays greeting', async () => {
68
91
})
69
92
```
70
93
94
+ ### Mock
95
+
96
+ Use the ` setupServer ` function from ` msw ` to mock an API request that our tested
97
+ component makes.
98
+
99
+ ``` js
100
+ // declare which API requests to mock
101
+ const server = setupServer (
102
+ // capture "GET /greeting" requests
103
+ rest .get (' /greeting' , (req , res , ctx ) => {
104
+ // respond using a mocked JSON body
105
+ return res (ctx .json ({ greeting: ' hello there' }))
106
+ })
107
+ )
108
+
109
+ // establish API mocking before all tests
110
+ beforeAll (() => server .listen ())
111
+ // reset any request handlers that are declared as a part of our tests
112
+ // (i.e. for testing one-time error scenarios)
113
+ afterEach (() => server .resetHandlers ())
114
+ // clean up once the tests are done
115
+ afterAll (() => server .close ())
116
+
117
+ // ...
118
+
119
+ test (' handlers server error' , async () => {
120
+ server .use (
121
+ // override the initial "GET /greeting" request handler
122
+ // to return a 500 Server Error
123
+ rest .get (' /greeting' , (req , res , ctx ) => {
124
+ return res (ctx .status (500 ))
125
+ })
126
+ )
127
+
128
+ // ...
129
+ })
130
+ ```
131
+
71
132
### Arrange
72
133
73
- The [ ` render ` ] ( ./api#render ) method renders a React element into the DOM and returns utility functions for testing the component.
134
+ The [ ` render ` ] ( ./api#render ) method renders a React element into the DOM and
135
+ returns utility functions for testing the component.
74
136
75
137
``` jsx
76
- const url = ' /greeting'
77
- const { container , asFragment } = render (< Fetch url= {url} / > )
138
+ const { container , asFragment } = render (< Fetch url= " /greeting" / > )
78
139
```
79
140
80
141
### Act
@@ -83,13 +144,9 @@ The [`fireEvent`](dom-testing-library/api-events.md) method allows you to fire
83
144
events to simulate user actions.
84
145
85
146
``` jsx
86
- axiosMock .get .mockResolvedValueOnce ({
87
- data: { greeting: ' hello there' },
88
- })
89
-
90
147
fireEvent .click (screen .getByText (' Load Greeting' ))
91
148
92
- // Wait until the mocked `get` request promise resolves and
149
+ // wait until the `get` request promise resolves and
93
150
// the component calls setState and re-renders.
94
151
// `waitFor` waits until the callback doesn't throw an error
95
152
@@ -107,16 +164,39 @@ fetch.js
107
164
import React , { useState } from ' react'
108
165
import axios from ' axios'
109
166
167
+ function greetingReducer (state , action ) {
168
+ switch (action .type ) {
169
+ case ' SUCCESS' : {
170
+ return {
171
+ error: null ,
172
+ greeting: action .greeting ,
173
+ }
174
+ }
175
+ case : ' ERROR' : {
176
+ error: action .error ,
177
+ greeting: null
178
+ }
179
+ default : {
180
+ return state
181
+ }
182
+ }
183
+ }
184
+
110
185
export default function Fetch ({ url }) {
111
- const [greeting , setGreeting ] = useState ( ' ' )
186
+ const [{ error , greeting }, dispatch ] = useReducer (greetingReducer )
112
187
const [buttonClicked , setButtonClicked ] = useState (false )
113
188
114
189
const fetchGreeting = async () => {
115
- const response = await axios .get (url)
116
- const data = response .data
117
- const { greeting } = data
118
- setGreeting (greeting)
119
- setButtonClicked (true )
190
+ axios .get (url)
191
+ .then ((response ) => {
192
+ const { data } = response
193
+ const { greeting } = data
194
+ dispatch ({ type: ' SUCCESS' , greeting })
195
+ setButtonClicked (true )
196
+ })
197
+ .catch ((error ) => {
198
+ dispatch ({ type: ' ERROR' })
199
+ })
120
200
}
121
201
122
202
const buttonText = buttonClicked ? ' Ok' : ' Load Greeting'
@@ -126,35 +206,9 @@ export default function Fetch({ url }) {
126
206
< button onClick= {fetchGreeting} disabled= {buttonClicked}>
127
207
{buttonText}
128
208
< / button>
129
- {greeting ? < h1> {greeting}< / h1> : null }
209
+ {greeting && < h1> {greeting}< / h1> }
210
+ {error && < p role= " alert" > Oops, failed to fetch! < / p> }
130
211
< / div>
131
212
)
132
213
}
133
214
```
134
-
135
- ``` jsx
136
- expect (axiosMock .get ).toHaveBeenCalledTimes (1 )
137
- expect (axiosMock .get ).toHaveBeenCalledWith (url)
138
- expect (screen .getByRole (' heading' )).toHaveTextContent (' hello there' )
139
- expect (screen .getByRole (' button' )).toHaveAttribute (' disabled' )
140
-
141
- // snapshots work great with regular DOM nodes!
142
- expect (container).toMatchInlineSnapshot (`
143
- <div>
144
- <div>
145
- <button
146
- disabled=""
147
- >
148
- Ok
149
- </button>
150
- <h1>
151
- hello there
152
- </h1>
153
- </div>
154
- </div>
155
- ` )
156
-
157
- // you can also use get a `DocumentFragment`,
158
- // which is useful if you want to compare nodes across render
159
- expect (asFragment ()).toMatchSnapshot ()
160
- ```
0 commit comments