epam-ai-conductor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/bin/workshop.js +6 -0
- package/dist/auth-check.d.ts +1 -0
- package/dist/auth-check.js +45 -0
- package/dist/auth-check.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +156 -0
- package/dist/cli.js.map +1 -0
- package/dist/content/content/module-1/task/module-1_home-task_components.md +257 -0
- package/dist/content/content/module-1/task/module-1_home-task_good_practices.md +69 -0
- package/dist/content/content/module-1/task/module-1_home-task_task.md +253 -0
- package/dist/content/content/module-1/tests/Button.test.tsx +22 -0
- package/dist/content/content/module-1/tests/CourseCard.test.tsx +64 -0
- package/dist/content/content/module-1/tests/CourseInfo.test.tsx +73 -0
- package/dist/content/content/module-1/tests/Courses.test.tsx +87 -0
- package/dist/content/content/module-1/tests/Header.test.tsx +23 -0
- package/dist/content/content/module-1/tests/Input.test.tsx +36 -0
- package/dist/content/content/module-1/tests/helpers.test.ts +13 -0
- package/dist/content/content/module-1/theory/module-1_elements-render.md +66 -0
- package/dist/content/content/module-1/theory/module-1_hooks_useEffect.md +178 -0
- package/dist/content/content/module-1/theory/module-1_hooks_useState.md +131 -0
- package/dist/content/content/module-1/theory/module-1_react.md +64 -0
- package/dist/content/content/module-1/theory/module-1_spa.md +74 -0
- package/dist/content/content/module-2/task/module-2_home-task_components.md +313 -0
- package/dist/content/content/module-2/task/module-2_home-task_good_practices.md +35 -0
- package/dist/content/content/module-2/task/module-2_home-task_task.md +210 -0
- package/dist/content/content/module-2/tests/App.test.tsx +54 -0
- package/dist/content/content/module-2/tests/Login.test.tsx +73 -0
- package/dist/content/content/module-2/tests/Registration.test.tsx +70 -0
- package/dist/content/content/module-2/tests/SearchBar.test.tsx +39 -0
- package/dist/content/content/module-2/theory/module-2_custom-hooks.md +201 -0
- package/dist/content/content/module-2/theory/module-2_hooks.md +117 -0
- package/dist/content/content/module-2/theory/module-2_react-router.md +328 -0
- package/dist/content/content/module-3/task/module-3_home-task_components.md +94 -0
- package/dist/content/content/module-3/task/module-3_home-task_good_practices.md +26 -0
- package/dist/content/content/module-3/task/module-3_home-task_task.md +170 -0
- package/dist/content/content/module-3/tests/App.test.tsx +54 -0
- package/dist/content/content/module-3/tests/Courses.test.tsx +87 -0
- package/dist/content/content/module-3/tests/CreateAuthor.test.tsx +44 -0
- package/dist/content/content/module-3/tests/CreateCourse.test.tsx +122 -0
- package/dist/content/content/module-3/theory/module-3_redux-hooks.md +194 -0
- package/dist/content/content/module-3/theory/module-3_state-actions-reducers.md +445 -0
- package/dist/content/content/module-4/task/module-4_home-task_components.md +187 -0
- package/dist/content/content/module-4/task/module-4_home-task_task.md +139 -0
- package/dist/content/content/module-4/tests/App.test.tsx +54 -0
- package/dist/content/content/module-4/tests/Courses.test.tsx +87 -0
- package/dist/content/content/module-4/tests/CreateCourse.test.tsx +122 -0
- package/dist/content/content/module-4/tests/Login.test.tsx +73 -0
- package/dist/content/content/module-4/theory/module-4_async-redux.md +99 -0
- package/dist/content/content/module-4/theory/module-4_private-routes.md +55 -0
- package/dist/content/content/module-5/task/module-5_home-task_instruction.md +68 -0
- package/dist/content/content/module-5/task/module-5_home-task_task.md +154 -0
- package/dist/content/content/module-5/tests/App.test.tsx +54 -0
- package/dist/content/content/module-5/tests/CourseCard.test.tsx +64 -0
- package/dist/content/content/module-5/tests/Courses.test.tsx +87 -0
- package/dist/content/content/module-5/tests/Header.test.tsx +23 -0
- package/dist/content/content/module-5/theory/module-5_react-testing-library_example.md +379 -0
- package/dist/content/content/module-5/theory/module-5_redux-writing-tests.md +246 -0
- package/dist/content/module-1/task/module-1_home-task_components.md +257 -0
- package/dist/content/module-1/task/module-1_home-task_good_practices.md +69 -0
- package/dist/content/module-1/task/module-1_home-task_task.md +253 -0
- package/dist/content/module-1/tests/Button.test.tsx +22 -0
- package/dist/content/module-1/tests/CourseCard.test.tsx +64 -0
- package/dist/content/module-1/tests/CourseInfo.test.tsx +73 -0
- package/dist/content/module-1/tests/Courses.test.tsx +87 -0
- package/dist/content/module-1/tests/Header.test.tsx +23 -0
- package/dist/content/module-1/tests/Input.test.tsx +36 -0
- package/dist/content/module-1/tests/helpers.test.ts +13 -0
- package/dist/content/module-1/theory/module-1_elements-render.md +66 -0
- package/dist/content/module-1/theory/module-1_hooks_useEffect.md +178 -0
- package/dist/content/module-1/theory/module-1_hooks_useState.md +131 -0
- package/dist/content/module-1/theory/module-1_react.md +64 -0
- package/dist/content/module-1/theory/module-1_spa.md +74 -0
- package/dist/content/module-2/task/module-2_home-task_components.md +313 -0
- package/dist/content/module-2/task/module-2_home-task_good_practices.md +35 -0
- package/dist/content/module-2/task/module-2_home-task_task.md +210 -0
- package/dist/content/module-2/tests/App.test.tsx +54 -0
- package/dist/content/module-2/tests/Login.test.tsx +73 -0
- package/dist/content/module-2/tests/Registration.test.tsx +70 -0
- package/dist/content/module-2/tests/SearchBar.test.tsx +39 -0
- package/dist/content/module-2/theory/module-2_custom-hooks.md +201 -0
- package/dist/content/module-2/theory/module-2_hooks.md +117 -0
- package/dist/content/module-2/theory/module-2_react-router.md +328 -0
- package/dist/content/module-3/task/module-3_home-task_components.md +94 -0
- package/dist/content/module-3/task/module-3_home-task_good_practices.md +26 -0
- package/dist/content/module-3/task/module-3_home-task_task.md +170 -0
- package/dist/content/module-3/tests/App.test.tsx +54 -0
- package/dist/content/module-3/tests/Courses.test.tsx +87 -0
- package/dist/content/module-3/tests/CreateAuthor.test.tsx +44 -0
- package/dist/content/module-3/tests/CreateCourse.test.tsx +122 -0
- package/dist/content/module-3/theory/module-3_redux-hooks.md +194 -0
- package/dist/content/module-3/theory/module-3_state-actions-reducers.md +445 -0
- package/dist/content/module-4/task/module-4_home-task_components.md +187 -0
- package/dist/content/module-4/task/module-4_home-task_task.md +139 -0
- package/dist/content/module-4/tests/App.test.tsx +54 -0
- package/dist/content/module-4/tests/Courses.test.tsx +87 -0
- package/dist/content/module-4/tests/CreateCourse.test.tsx +122 -0
- package/dist/content/module-4/tests/Login.test.tsx +73 -0
- package/dist/content/module-4/theory/module-4_async-redux.md +99 -0
- package/dist/content/module-4/theory/module-4_private-routes.md +55 -0
- package/dist/content/module-5/task/module-5_home-task_instruction.md +68 -0
- package/dist/content/module-5/task/module-5_home-task_task.md +154 -0
- package/dist/content/module-5/tests/App.test.tsx +54 -0
- package/dist/content/module-5/tests/CourseCard.test.tsx +64 -0
- package/dist/content/module-5/tests/Courses.test.tsx +87 -0
- package/dist/content/module-5/tests/Header.test.tsx +23 -0
- package/dist/content/module-5/theory/module-5_react-testing-library_example.md +379 -0
- package/dist/content/module-5/theory/module-5_redux-writing-tests.md +246 -0
- package/dist/content-loader.d.ts +7 -0
- package/dist/content-loader.js +26 -0
- package/dist/content-loader.js.map +1 -0
- package/dist/context-builder.d.ts +2 -0
- package/dist/context-builder.js +116 -0
- package/dist/context-builder.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_position: 2
|
|
3
|
+
sidebar_label: 'React Testing Library: Examples'
|
|
4
|
+
title: 'Examples of Tests'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
In this section, you will learn how to render a React component in test with [React Testing Library](https://github.com/testing-library/react-testing-library).
|
|
8
|
+
|
|
9
|
+
## Rendering a component
|
|
10
|
+
|
|
11
|
+
We will use the following App [function component](https://www.robinwieruch.de/react-function-component/) from a `src/App.js` file:
|
|
12
|
+
|
|
13
|
+
```jsx title="src/App.js"
|
|
14
|
+
const title = 'React Testing Library Example';
|
|
15
|
+
|
|
16
|
+
function App() {
|
|
17
|
+
return <div>{title}</div>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default App;
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And test it in a `src/App.test.js` file:
|
|
24
|
+
|
|
25
|
+
```jsx title="src/App.test.js"
|
|
26
|
+
import { render } from '@testing-library/react';
|
|
27
|
+
|
|
28
|
+
import App from './App';
|
|
29
|
+
|
|
30
|
+
describe('App', () => {
|
|
31
|
+
test('renders App component', () => {
|
|
32
|
+
render(<App />);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The test code above used React Testing Library's `render` method to virtually render the App component imported from `App.js` file and append it to the `document.body` node. You can access the rendered HTML through the `screen` object. This object also has the DOM testing methods already bound into it.
|
|
38
|
+
|
|
39
|
+
RTL's render function takes any [JSX](../module-1/jsx.mdx) to render it. Afterward, you should have access to the React component in your test. To convince yourself that it's there, you can use RTL's [debug](https://subscription.packtpub.com/book/web_development/9781800564459/2/ch02lvl1sec14/using-the-debug-method) function:
|
|
40
|
+
|
|
41
|
+
```jsx title="src/App.test.js"
|
|
42
|
+
// highlight-next-line
|
|
43
|
+
import { render, screen } from '@testing-library/react';
|
|
44
|
+
|
|
45
|
+
import App from './App';
|
|
46
|
+
|
|
47
|
+
describe('App', () => {
|
|
48
|
+
test('renders App component', () => {
|
|
49
|
+
render(<App />);
|
|
50
|
+
|
|
51
|
+
// highlight-next-line
|
|
52
|
+
screen.debug();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
After running your test on the command line, you should see the HTML output of your App component. Whenever you write a test for a component with React Testing library, you can render the component first and then debug what's visible for RTL's renderer in the test. This way, you can write your test with more confidence:
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<body>
|
|
61
|
+
<div>
|
|
62
|
+
<div>
|
|
63
|
+
React Testing Library Example
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</body>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The great thing about it, React Testing Library doesn't care much about the actual components. Let's take the following React components which utilize different React features ([useState](../module-1/hooks/useState.md), [event handler](https://www.robinwieruch.de/react-event-handler/), [props]("../module-1/components-and-props.md")) and concepts ([controlled component](https://www.robinwieruch.de/react-controlled-components/)):
|
|
70
|
+
|
|
71
|
+
```jsx
|
|
72
|
+
function App() {
|
|
73
|
+
const [search, setSearch] = React.useState('');
|
|
74
|
+
|
|
75
|
+
function handleChange(event) {
|
|
76
|
+
setSearch(event.target.value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div>
|
|
81
|
+
<Search value={search} onChange={handleChange}>
|
|
82
|
+
Search:
|
|
83
|
+
</Search>
|
|
84
|
+
|
|
85
|
+
<p>Searches for {search ? search : '...'}</p>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Search({ value, onChange, children }) {
|
|
91
|
+
return (
|
|
92
|
+
<div>
|
|
93
|
+
<label htmlFor="search">{children}</label>
|
|
94
|
+
<input
|
|
95
|
+
id="search"
|
|
96
|
+
type="text"
|
|
97
|
+
value={value}
|
|
98
|
+
onChange={onChange}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default App;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
If you start the test of your App component again, you should see the following output from the `debug` function:
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<body>
|
|
111
|
+
<div>
|
|
112
|
+
<div>
|
|
113
|
+
<div>
|
|
114
|
+
<label
|
|
115
|
+
for="search"
|
|
116
|
+
>
|
|
117
|
+
Search:
|
|
118
|
+
</label>
|
|
119
|
+
<input
|
|
120
|
+
id="search"
|
|
121
|
+
type="text"
|
|
122
|
+
value=""
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
<p>
|
|
126
|
+
Searches for
|
|
127
|
+
...
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</body>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
React Testing Library is used to interact with your React components like a human being. What a human being sees is just rendered HTML from your React components, so that's why you see this HTML structure as output rather than two individual React components.
|
|
135
|
+
|
|
136
|
+
## Selecting elements
|
|
137
|
+
|
|
138
|
+
Let's learn about how to grab elements which are then used for assertions or for user interactions:
|
|
139
|
+
|
|
140
|
+
```jsx
|
|
141
|
+
import React from 'react';
|
|
142
|
+
import { render, screen } from '@testing-library/react';
|
|
143
|
+
|
|
144
|
+
import App from './App';
|
|
145
|
+
|
|
146
|
+
describe('App', () => {
|
|
147
|
+
test('renders App component', () => {
|
|
148
|
+
render(<App />);
|
|
149
|
+
|
|
150
|
+
// highlight-next-line
|
|
151
|
+
screen.getByText('Search:');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
And this is an example of the assertion that checks whether the element is in the DOM:
|
|
157
|
+
|
|
158
|
+
```jsx
|
|
159
|
+
import React from 'react';
|
|
160
|
+
import { render, screen } from '@testing-library/react';
|
|
161
|
+
|
|
162
|
+
import App from './App';
|
|
163
|
+
|
|
164
|
+
describe('App', () => {
|
|
165
|
+
test('renders App component', () => {
|
|
166
|
+
render(<App />);
|
|
167
|
+
|
|
168
|
+
// highlight-next-line
|
|
169
|
+
expect(screen.getByText('Search:')).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Conveniently `getByText` throws an error by default if the element cannot be found. This is useful for giving you a hint while writing the test that the selected element isn't there in the first place. A few people exploit this behavior to use search functions like `getByText` as implicit assertion replacement instead of an explicit assertion with expect.
|
|
175
|
+
|
|
176
|
+
Most of React test cases should use methods for finding elements. RTL provides you with several methods to find an element by specific attributes in addition to the `getByText()` method above:
|
|
177
|
+
|
|
178
|
+
* `getByText()`: find the element by its `textContent` value
|
|
179
|
+
* `getByRole()`: by its `role` attribute value
|
|
180
|
+
* `getByLabelText()`: by its `label` attribute value
|
|
181
|
+
* `getByPlaceholderText()`: by its `placeholder` attribute value
|
|
182
|
+
* `getByAltText()`: by its `alt` attribute value
|
|
183
|
+
* `getByDisplayValue()`: by its `value` attribute, usually for `<input>` elements
|
|
184
|
+
* `getByTitle()`: by its `title` attribute value
|
|
185
|
+
|
|
186
|
+
And when these methods are not enough, you can use the `getByTestId()` method, which allows you to find an element by its `data-testid` attribute:
|
|
187
|
+
|
|
188
|
+
```jsx
|
|
189
|
+
import { render, screen } from '@testing-library/react';
|
|
190
|
+
|
|
191
|
+
render(<div data-testid="test-element" />);
|
|
192
|
+
const element = screen.getByTestId('test-element');
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
:::tip
|
|
196
|
+
But since selecting elements using `data-testid` attributes doesn't resemble how a real user would use your application, the documentation recommends you use it only as the last resort when all other methods fail to find your element. Generally, finding by Text, Role, or Label should cover most cases.
|
|
197
|
+
:::
|
|
198
|
+
|
|
199
|
+
## Search variants
|
|
200
|
+
|
|
201
|
+
One of the search variants in RTL is `getBy` which is used for `getByText` or `getByRole`. This is also the search variant which is used by default when testing React components.
|
|
202
|
+
|
|
203
|
+
Two other search variants are `queryBy` and `findBy`; which both can get extended by the same search types that `getBy` has access to. For example, `queryBy` with all its search types:
|
|
204
|
+
|
|
205
|
+
* queryByText
|
|
206
|
+
* queryByRole
|
|
207
|
+
* queryByLabelText
|
|
208
|
+
* queryByPlaceholderText
|
|
209
|
+
* queryByAltText
|
|
210
|
+
* queryByDisplayValue
|
|
211
|
+
|
|
212
|
+
And `findBy` with all its search types:
|
|
213
|
+
|
|
214
|
+
* findByText
|
|
215
|
+
* findByRole
|
|
216
|
+
* findByLabelText
|
|
217
|
+
* findByPlaceholderText
|
|
218
|
+
* findByAltText
|
|
219
|
+
* findByDisplayValue
|
|
220
|
+
|
|
221
|
+
### What's the difference between getBy vs queryBy?
|
|
222
|
+
|
|
223
|
+
You already know that `getBy` returns an element or an error. It's a convenient side-effect of `getBy` that it returns an error, because it makes sure that we as developers notice early that there is something wrong in our test. However, this makes it difficult to check for elements which shouldn't be there:
|
|
224
|
+
|
|
225
|
+
```jsx
|
|
226
|
+
import React from 'react';
|
|
227
|
+
import { render, screen } from '@testing-library/react';
|
|
228
|
+
|
|
229
|
+
import App from './App';
|
|
230
|
+
|
|
231
|
+
describe('App', () => {
|
|
232
|
+
test('renders App component', () => {
|
|
233
|
+
render(<App />);
|
|
234
|
+
|
|
235
|
+
screen.debug();
|
|
236
|
+
|
|
237
|
+
// fails
|
|
238
|
+
expect(screen.getByText(/Searches for JavaScript/)).toBeNull();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
This doesn't work, because, even though debug output shows that the element with the text "Searches for JavaScript" isn't there, `getBy` throws an error before we can make the assertion, because it cannot find the element with this text. In order to assert elements which aren't there, we can exchange `getBy` with `queryBy`:
|
|
244
|
+
|
|
245
|
+
```jsx
|
|
246
|
+
import React from 'react';
|
|
247
|
+
import { render, screen } from '@testing-library/react';
|
|
248
|
+
|
|
249
|
+
import App from './App';
|
|
250
|
+
|
|
251
|
+
describe('App', () => {
|
|
252
|
+
test('renders App component', () => {
|
|
253
|
+
render(<App />);
|
|
254
|
+
|
|
255
|
+
// highlight-next-line
|
|
256
|
+
expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
So every time you are asserting that an element isn't there, use `queryBy`. Otherwise default to `getBy`. So what about `findBy` then? The `findBy` search variant is used for asynchronous elements which will be there eventually.
|
|
262
|
+
|
|
263
|
+
### What about multiple elements?
|
|
264
|
+
|
|
265
|
+
You have learned about the three search variants `getBy`, `queryBy` and `findBy`; which all can be associated with the search types (e.g. Text, Role, PlaceholderText, DisplayValue). If all of these search functions return only one element, how to assert if there are multiple elements (e.g. a list in a React component). All search variants can be extended with the `All` word:
|
|
266
|
+
|
|
267
|
+
* getAllBy
|
|
268
|
+
* queryAllBy
|
|
269
|
+
* findAllBy
|
|
270
|
+
|
|
271
|
+
Whereas all of them return an array of elements and can be associated with the search types again.
|
|
272
|
+
|
|
273
|
+
### Query types summary table
|
|
274
|
+
|
|
275
|
+
<table>
|
|
276
|
+
<thead>
|
|
277
|
+
<tr>
|
|
278
|
+
<th>Type of Query</th>
|
|
279
|
+
<th>0 Matches</th>
|
|
280
|
+
<th>1 Match</th>
|
|
281
|
+
<th>>1 Matches</th>
|
|
282
|
+
<th align="center">Retry (Async/Await)</th>
|
|
283
|
+
</tr>
|
|
284
|
+
</thead>
|
|
285
|
+
<tbody>
|
|
286
|
+
<tr>
|
|
287
|
+
<td><strong>Single Element</strong></td>
|
|
288
|
+
<td></td>
|
|
289
|
+
<td></td>
|
|
290
|
+
<td></td>
|
|
291
|
+
<td align="center"></td>
|
|
292
|
+
</tr>
|
|
293
|
+
<tr>
|
|
294
|
+
<td><code>getBy...</code></td>
|
|
295
|
+
<td>Throw error</td>
|
|
296
|
+
<td>Return element</td>
|
|
297
|
+
<td>Throw error</td>
|
|
298
|
+
<td align="center">No</td>
|
|
299
|
+
</tr>
|
|
300
|
+
<tr>
|
|
301
|
+
<td><code>queryBy...</code></td>
|
|
302
|
+
<td>Return <code>null</code></td>
|
|
303
|
+
<td>Return element</td>
|
|
304
|
+
<td>Throw error</td>
|
|
305
|
+
<td align="center">No</td>
|
|
306
|
+
</tr>
|
|
307
|
+
<tr>
|
|
308
|
+
<td><code>findBy...</code></td>
|
|
309
|
+
<td>Throw error</td>
|
|
310
|
+
<td>Return element</td>
|
|
311
|
+
<td>Throw error</td>
|
|
312
|
+
<td align="center">Yes</td>
|
|
313
|
+
</tr>
|
|
314
|
+
<tr>
|
|
315
|
+
<td><strong>Multiple Elements</strong></td>
|
|
316
|
+
<td></td>
|
|
317
|
+
<td></td>
|
|
318
|
+
<td></td>
|
|
319
|
+
<td align="center"></td>
|
|
320
|
+
</tr>
|
|
321
|
+
<tr>
|
|
322
|
+
<td><code>getAllBy...</code></td>
|
|
323
|
+
<td>Throw error</td>
|
|
324
|
+
<td>Return array</td>
|
|
325
|
+
<td>Return array</td>
|
|
326
|
+
<td align="center">No</td>
|
|
327
|
+
</tr>
|
|
328
|
+
<tr>
|
|
329
|
+
<td><code>queryAllBy...</code></td>
|
|
330
|
+
<td>Return <code>[]</code></td>
|
|
331
|
+
<td>Return array</td>
|
|
332
|
+
<td>Return array</td>
|
|
333
|
+
<td align="center">No</td>
|
|
334
|
+
</tr>
|
|
335
|
+
<tr>
|
|
336
|
+
<td><code>findAllBy...</code></td>
|
|
337
|
+
<td>Throw error</td>
|
|
338
|
+
<td>Return array</td>
|
|
339
|
+
<td>Return array</td>
|
|
340
|
+
<td align="center">Yes</td>
|
|
341
|
+
</tr>
|
|
342
|
+
</tbody>
|
|
343
|
+
</table>
|
|
344
|
+
|
|
345
|
+
## Fire event
|
|
346
|
+
|
|
347
|
+
We can use RTL's `fireEvent` function to simulate interactions of an end user. Let's see how this works for our input field:
|
|
348
|
+
|
|
349
|
+
```jsx
|
|
350
|
+
import React from 'react';
|
|
351
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
352
|
+
|
|
353
|
+
import App from './App';
|
|
354
|
+
|
|
355
|
+
describe('App', () => {
|
|
356
|
+
test('renders App component', () => {
|
|
357
|
+
render(<App />);
|
|
358
|
+
|
|
359
|
+
screen.debug();
|
|
360
|
+
|
|
361
|
+
fireEvent.change(screen.getByRole('textbox'), {
|
|
362
|
+
target: { value: 'JavaScript' },
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
screen.debug();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The fireEvent function takes an element (here the input field by textbox role) and an event (here an event which has the value "JavaScript"). The debug function's output should show the HTML structure before and after the event; and you should see that the new value of the input field gets rendered appropriately.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Materials
|
|
375
|
+
1. [React Testing Library Examples](https://github.com/testing-library/react-testing-library#examples)
|
|
376
|
+
2. [React Testing Library Example: Step-By-Step](https://testing-library.com/docs/react-testing-library/example-intro)
|
|
377
|
+
3. [React Testing Library: Queries](https://testing-library.com/docs/react-testing-library/cheatsheet#queries)
|
|
378
|
+
4. [Firing Events](https://testing-library.com/docs/dom-testing-library/api-events)
|
|
379
|
+
5. [Testing Recipes](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_position: 4
|
|
3
|
+
sidebar_label: 'Redux. Writing Tests'
|
|
4
|
+
title: Redux. Writing Tests
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
The general advice for testing an app using Redux is as follows:
|
|
8
|
+
|
|
9
|
+
* Use integration tests for everything working together. I.e. for a React app using Redux, render a `<Provider>` with a real store instance wrapping the component/s being tested. Interactions with the page being tested should use real Redux logic, with API calls mocked out so app code doesn't have to change, and assert that the UI is updated appropriately.
|
|
10
|
+
* If needed, use basic unit tests for pure functions such as particularly complex reducers or selectors. However, in many cases, these are just implementation details that are covered by integration tests instead.
|
|
11
|
+
|
|
12
|
+
## Testing Redux connected components
|
|
13
|
+
|
|
14
|
+
Let's say we have some components, which fetch user data and display it.
|
|
15
|
+
|
|
16
|
+
```jsx
|
|
17
|
+
import React, { useState, useEffect } from "react";
|
|
18
|
+
import { getUserData } from "./api";
|
|
19
|
+
|
|
20
|
+
const User = () => {
|
|
21
|
+
const [userData, setUserData] = useState(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
getUserData.then((data) => {
|
|
25
|
+
setUserData(data);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!userData) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return <div>{userData.name}</div>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default User;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Now let's see how it can be tested
|
|
40
|
+
|
|
41
|
+
```jsx
|
|
42
|
+
import React from "react";
|
|
43
|
+
import { screen, render } from "@testing-library/react";
|
|
44
|
+
import User from "./User";
|
|
45
|
+
|
|
46
|
+
jest.mock("./api", () => ({
|
|
47
|
+
getUserData: () => ({ name: "mock name" })
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
describe("User", () => {
|
|
51
|
+
it("should display user name", async () => {
|
|
52
|
+
render(<User />);
|
|
53
|
+
|
|
54
|
+
const userName = await screen.findByText("mock name");
|
|
55
|
+
|
|
56
|
+
expect(userName).toBeTruthy();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The first thing we have to do is mock our API call with `jest.mock`. Normally it would make a network request, but in tests, we need to mock it.
|
|
62
|
+
|
|
63
|
+
Then we use `render` function to render our component, and `screen.findByText` to search for text in the component we just rendered.
|
|
64
|
+
|
|
65
|
+
Now let's suppose we would need to access user data in other parts of the application. Let's move it to the Redux store. Here is how the refactored version of the component might look like.
|
|
66
|
+
|
|
67
|
+
```jsx
|
|
68
|
+
import React, { useState, useEffect } from "react";
|
|
69
|
+
import { connect } from "react-redux";
|
|
70
|
+
import { fetchUserData } from './actions';
|
|
71
|
+
|
|
72
|
+
const User = ({ userData, fetchUserData }) => {
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
fetchUserData();
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
if (!userData) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return <div>{userData.name}</div>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const mapStateToProps = (state) => ({
|
|
85
|
+
userData: state.user
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const mapDispatchToProps = {
|
|
89
|
+
fetchUserData,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default connect(mapStateToProps, mapDispatchToProps)(User);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Now the first thing you will notice in your test is: **'Could not find "store" in the context of "Connect(User)"'** error. It's because your component needs to be wrapper in `Provider` to access Redux Store. Let's fix our tests:
|
|
96
|
+
|
|
97
|
+
```jsx
|
|
98
|
+
import React from "react";
|
|
99
|
+
import { screen, render } from "@testing-library/react";
|
|
100
|
+
import { createStore } from "redux";
|
|
101
|
+
import User from "./User";
|
|
102
|
+
import reducer from "./reducer";
|
|
103
|
+
import store from "./store";
|
|
104
|
+
|
|
105
|
+
jest.mock("./api", () => ({
|
|
106
|
+
getUserData: () => ({ name: "mock name" })
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const initialState = {
|
|
110
|
+
user: { name: "mock name" },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const store = createStore(reducer, initialState);
|
|
114
|
+
|
|
115
|
+
const Wrapper = ({ children }) => (
|
|
116
|
+
<Provider store={store}>{children}</Provider>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
describe("User", () => {
|
|
120
|
+
it("should display user name", async () => {
|
|
121
|
+
render(<User />, { wrapper: Wrapper });
|
|
122
|
+
|
|
123
|
+
const userName = await screen.findByText("mock name");
|
|
124
|
+
|
|
125
|
+
expect(userName).toBeTruthy();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
We fixed the error by creating a Wrapper. This component will wrap the component we test with Provider and apply a mocked state. We can go one step further by custom render function using the one from React Testing Library.
|
|
131
|
+
|
|
132
|
+
```jsx
|
|
133
|
+
import React from "react";
|
|
134
|
+
import { render as rtlRender } from "@testing-library/react";
|
|
135
|
+
import { createStore } from "redux";
|
|
136
|
+
import { Provider } from "react-redux";
|
|
137
|
+
import reducer from "./reducer";
|
|
138
|
+
|
|
139
|
+
export const renderWithState = (
|
|
140
|
+
ui,
|
|
141
|
+
{ initialState, ...renderOptions } = {}
|
|
142
|
+
) => {
|
|
143
|
+
const store = createStore(reducer, initialState);
|
|
144
|
+
const Wrapper = ({ children }) => (
|
|
145
|
+
<Provider store={store}>{children}</Provider>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return render(ui, { wrapper: Wrapper, ...renderOptions });
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
And then in our case we can just import it and use like this:
|
|
153
|
+
|
|
154
|
+
```jsx
|
|
155
|
+
renderWithState(<User />, { initialState });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Reducers
|
|
159
|
+
|
|
160
|
+
Reducers are simple functions that give you the new state when you apply an action to the current state. Usually, you don't need to test reducers directly unless they have complex logic. But, if you want to be sure your reducer works as expected, testing is straightforward.
|
|
161
|
+
|
|
162
|
+
Since reducers are pure functions, testing them is simple. Just call the reducer with a certain starting state and action, then check if the resulting state is what you expect.
|
|
163
|
+
```jsx
|
|
164
|
+
import { createSlice } from '@reduxjs/toolkit'
|
|
165
|
+
|
|
166
|
+
const initialState = [
|
|
167
|
+
{
|
|
168
|
+
text: 'Use Redux',
|
|
169
|
+
completed: false,
|
|
170
|
+
id: 0
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
const todosSlice = createSlice({
|
|
175
|
+
name: 'todos',
|
|
176
|
+
initialState,
|
|
177
|
+
reducers: {
|
|
178
|
+
todoAdded(state, action: PayloadAction<string>) {
|
|
179
|
+
state.push({
|
|
180
|
+
id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
|
|
181
|
+
completed: false,
|
|
182
|
+
text: action.payload
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
export const { todoAdded } = todosSlice.actions
|
|
189
|
+
|
|
190
|
+
export default todosSlice.reducer
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
We can test it like:
|
|
194
|
+
|
|
195
|
+
```jsx
|
|
196
|
+
import reducer, { todoAdded } from './todosSlice'
|
|
197
|
+
|
|
198
|
+
test('should return the initial state', () => {
|
|
199
|
+
expect(reducer(undefined, {})).toEqual([
|
|
200
|
+
{
|
|
201
|
+
text: 'Use Redux',
|
|
202
|
+
completed: false,
|
|
203
|
+
id: 0
|
|
204
|
+
}
|
|
205
|
+
])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('should handle a todo being added to an empty list', () => {
|
|
209
|
+
const previousState = []
|
|
210
|
+
expect(reducer(previousState, todoAdded('Run the tests'))).toEqual([
|
|
211
|
+
{
|
|
212
|
+
text: 'Run the tests',
|
|
213
|
+
completed: false,
|
|
214
|
+
id: 0
|
|
215
|
+
}
|
|
216
|
+
])
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('should handle a todo being added to an existing list', () => {
|
|
220
|
+
const previousState = [
|
|
221
|
+
{
|
|
222
|
+
text: 'Run the tests',
|
|
223
|
+
completed: true,
|
|
224
|
+
id: 0
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
expect(reducer(previousState, todoAdded('Use Redux'))).toEqual([
|
|
228
|
+
{
|
|
229
|
+
text: 'Run the tests',
|
|
230
|
+
completed: true,
|
|
231
|
+
id: 0
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
text: 'Use Redux',
|
|
235
|
+
completed: false,
|
|
236
|
+
id: 1
|
|
237
|
+
}
|
|
238
|
+
])
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
----
|
|
243
|
+
|
|
244
|
+
## Materials
|
|
245
|
+
1. [Redux tests](https://redux.js.org/usage/writing-tests)
|
|
246
|
+
2. [How to test React-Redux connected Components](https://www.robinwieruch.de/react-connected-component-test)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const CONTENT_DIR = join(__dirname, 'content');
|
|
6
|
+
export const TOTAL_MODULES = 5;
|
|
7
|
+
function readDir(dir) {
|
|
8
|
+
if (!existsSync(dir))
|
|
9
|
+
return '';
|
|
10
|
+
return readdirSync(dir)
|
|
11
|
+
.sort()
|
|
12
|
+
.map(f => {
|
|
13
|
+
const label = f.replace(/^module-\d+_/, '').replace(/_/g, '/');
|
|
14
|
+
return `### ${label}\n\n${readFileSync(join(dir, f), 'utf-8')}`;
|
|
15
|
+
})
|
|
16
|
+
.join('\n\n---\n\n');
|
|
17
|
+
}
|
|
18
|
+
export function loadModule(n) {
|
|
19
|
+
const base = join(CONTENT_DIR, `module-${n}`);
|
|
20
|
+
return {
|
|
21
|
+
theory: readDir(join(base, 'theory')),
|
|
22
|
+
task: readDir(join(base, 'task')),
|
|
23
|
+
tests: readDir(join(base, 'tests')),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=content-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-loader.js","sourceRoot":"","sources":["../src/content-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AAE/C,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC;AAQ/B,SAAS,OAAO,CAAC,GAAW;IAC1B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,OAAO,WAAW,CAAC,GAAG,CAAC;SACpB,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,CAAC,EAAE;QACP,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC/D,OAAO,OAAO,KAAK,OAAO,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC;IAClE,CAAC,CAAC;SACD,IAAI,CAAC,aAAa,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,CAAS;IAClC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC;IAC9C,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACrC,IAAI,EAAI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACnC,KAAK,EAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;KACrC,CAAC;AACJ,CAAC"}
|