396 |
397 | `;
398 | ```
399 |
400 | 보시면, `mount` 의 경우 Profile 내부의 내용까지 전부 렌더링 된 반면, `shallow` 에선 이 작업이 생략됐지요? 추가적으로, `mount` 에서는 최상위 요소가 Counter 컴포넌트인 반면에, `shallow` 에서는 최상위 요소가 div 입니다. 따라서, `shallow` 를 할 경우 `wrapper.props()` 를 조회하게 되면 컴포넌트의 props 가 나타나는 것이 아니라 div 의 props 가 나타나게 됩니다.
401 |
402 | ```js
403 | expect(wrapper.state().number).toBe(0);
404 | ```
405 |
406 | 컴포넌트의 state 를 조회 할 때에는 위와 같이 `state()` 함수를 사용합니다.
407 |
408 | ```js
409 | wrapper.instance().handleIncrease();
410 | ```
411 |
412 | 그리고, 내장 메서드를 호출할때에는 `instance()` 함수를 호출하여 인스턴스를 조회 후 메서드를 호출 할 수 있습니다.
413 |
414 | ## DOM 이벤트 시뮬레이트
415 |
416 | 이번에는 내장 메서드를 직접 호출하는게 아니라, 버튼 클릭 이벤트를 시뮬레이트하여 기능이 잘 작동하는지 확인해보겠습니다.
417 |
418 | #### `Counter.test.js`
419 |
420 | ```jsx
421 | import React from 'react';
422 | import { shallow } from 'enzyme';
423 | import Counter from './Counter';
424 |
425 | describe('', () => {
426 | it('matches snapshot', () => {
427 | const wrapper = shallow();
428 | expect(wrapper).toMatchSnapshot();
429 | });
430 | it('has initial number', () => {
431 | const wrapper = shallow();
432 | expect(wrapper.state().number).toBe(0);
433 | });
434 | it('increases', () => {
435 | const wrapper = shallow();
436 | wrapper.instance().handleIncrease();
437 | expect(wrapper.state().number).toBe(1);
438 | });
439 | it('decreases', () => {
440 | const wrapper = shallow();
441 | wrapper.instance().handleDecrease();
442 | expect(wrapper.state().number).toBe(-1);
443 | });
444 | it('calls handleIncrease', () => {
445 | // 클릭이벤트를 시뮬레이트하고, state 를 확인
446 | const wrapper = shallow();
447 | const plusButton = wrapper.findWhere(
448 | node => node.type() === 'button' && node.text() === '+1'
449 | );
450 | plusButton.simulate('click');
451 | expect(wrapper.state().number).toBe(1);
452 | });
453 | it('calls handleDecrease', () => {
454 | // 클릭 이벤트를 시뮬레이트하고, h2 태그의 텍스트 확인
455 | const wrapper = shallow();
456 | const minusButton = wrapper.findWhere(
457 | node => node.type() === 'button' && node.text() === '-1'
458 | );
459 | minusButton.simulate('click');
460 | const number = wrapper.find('h2');
461 | expect(number.text()).toBe('-1');
462 | });
463 | });
464 | ```
465 |
466 | 위 테스트 케이스에서는 [`findWhere()`](https://airbnb.io/enzyme/docs/api/ShallowWrapper/findWhere.html) 함수를 사용하여 우리가 원하는 버튼 태그를 선택해주었습니다. 이 함수를 사용하면 우리가 원하는 조건을 만족하는 태그를 선택 할 수 있습니다.
467 |
468 | 만약에 `findWhere()` 를 사용하지 않는다면 다음과 같이 코드를 작성해야합니다.
469 |
470 | ```js
471 | const buttons = wrapper.find('button');
472 | const plusButton = buttons.get(0); // 첫번째 버튼 +1
473 | const minusButton = buttons.get(1); // 두번째 버튼 -1
474 | ```
475 |
476 | 버튼에 이벤트를 시뮬레이트 할 때에는 원하는 엘리먼트를 찾아서 `simulate()` 함수를 사용합니다. 첫번째 파라미터에는 이벤트 이름을 넣고 두번째 파라미터에는 이벤트 객체를 넣습니다. 만약에 인풋에 change 이벤트를 발생시키는 경우엔 다음과 같이 하면 됩니다.
477 |
478 | ```javascript
479 | input.simulate('change', {
480 | target: {
481 | value: 'hello world'
482 | }
483 | });
484 | ```
485 |
486 | 그리고, 값이 잘 업데이트 됐는지 확인하기 위해서 두가지 방법을 사용했는데요, 첫번째 방법은 state 를 직접 조회하는 것이고, 두번째 방법은 h2 태그를 조회해서 값을 확인하는 것 입니다. 실제 테스트 코드를 작성하게 될 때에는 이 방법 중 아무거나 선택하셔도 상관없습니다.
487 |
488 | ## 함수형 컴포넌트와 Hooks 테스팅
489 |
490 | 이번에는 Hooks 를 사용하는 함수형 컴포넌트의 테스트 코드를 작성해봅시다. HookCounter 라는 컴포넌트를 만들어보세요.
491 |
492 | #### `src/HookCounter.js`
493 |
494 | ```jsx
495 | import React, { useState, useCallback } from 'react';
496 |
497 | const HookCounter = () => {
498 | const [number, setNumber] = useState(0);
499 | const onIncrease = useCallback(() => {
500 | setNumber(number + 1);
501 | }, [number]);
502 | const onDecrease = useCallback(() => {
503 | setNumber(number - 1);
504 | }, [number]);
505 |
506 | return (
507 |
508 |
{number}
509 |
510 |
511 |
512 | );
513 | };
514 |
515 | export default HookCounter;
516 | ```
517 |
518 | 컴포넌트를 만들고 App 에서 렌더링하여 잘 작동하는지 직접 먼저 확인해보세요.
519 |
520 | #### `src/App.js`
521 |
522 | ```jsx
523 | import React from 'react';
524 |
525 | import HookCounter from './HookCounter';
526 |
527 | function App() {
528 | return (
529 |
530 |
531 |
532 | );
533 | }
534 |
535 | export default App;
536 | ```
537 |
538 | 이 컴포넌트를 위한 테스트케이스를 작성해보겠습니다. 함수형 컴포넌트에서는 클래스형 컴포넌트와 달리 인스턴스 메서드 및 상태를 조회 할 방법이 없습니다. 추가적으로, Hooks 를 사용하는 경우 꼭 `shallow` 가 아닌 `mount` 를 사용하셔야 합니다. 그 이유는, `useEffect` Hook 은 `shallow` 에서 작동하지 않고, 버튼 엘리먼트에 연결되어있는 함수가 이전 함수를 가르키고 있기 때문에, 예를 들어 +1 버튼의 클릭 이벤트를 두번 시뮬레이트해도 결과값이 2가 되는게 아니라 1이 됩니다.
539 |
540 | #### `HookCounter.test.js`
541 |
542 | ```javascript
543 | import React from 'react';
544 | import { mount } from 'enzyme';
545 | import HookCounter from './HookCounter';
546 |
547 | describe('', () => {
548 | it('matches snapshot', () => {
549 | const wrapper = mount();
550 | expect(wrapper).toMatchSnapshot();
551 | });
552 | it('increases', () => {
553 | const wrapper = mount();
554 | let plusButton = wrapper.findWhere(
555 | node => node.type() === 'button' && node.text() === '+1'
556 | );
557 | plusButton.simulate('click');
558 | plusButton.simulate('click');
559 |
560 | const number = wrapper.find('h2');
561 |
562 | expect(number.text()).toBe('2');
563 | });
564 | it('decreases', () => {
565 | const wrapper = mount();
566 | let decreaseButton = wrapper.findWhere(
567 | node => node.type() === 'button' && node.text() === '-1'
568 | );
569 | decreaseButton.simulate('click');
570 | decreaseButton.simulate('click');
571 |
572 | const number = wrapper.find('h2');
573 |
574 | expect(number.text()).toBe('-2');
575 | });
576 | });
577 | ```
578 |
579 | ## 정리
580 |
581 | 이번 섹션에서는 Enzyme 을 통한 컴포넌트 테스팅에 대해서 알아보았습니다. Enzyme 의 [공식 문서](https://airbnb.io/enzyme/docs/api/)를 보면, Enzyme 에 있는 더 많은 기능들을 볼 수 있습니다.
582 |
--------------------------------------------------------------------------------
/docs/05-react-testing-library.md:
--------------------------------------------------------------------------------
1 | # 5. react-testing-library 사용법
2 |
3 | [react-testing-library](https://testing-library.com/docs/react-testing-library/intro) 에서는 Enzyme 과 달리 모든 테스트를 DOM 위주로 진행합니다. 그리고, 컴포넌트의 props 나 state 를 조회하는 일은 없습니다. 컴포넌트를 리팩토링하게 될 때에는, 주로 내부 구조 및 네이밍은 많이 바뀔 수 있어도 실제 작동 방식은 크게 바뀌지 않습니다. react-testing-library는 이 점을 중요시 여겨서, 컴포넌트의 기능이 똑같이 작동한다면 컴포넌트의 내부 구현 방식이 많이 바뀌어도 테스트가 실패하지 않도록 설계되었습니다. 추가적으로, Enzyme 은 엄청나게 다양한 기능을 제공하는 반면, react-testing-library 에는 정말 필요한 기능들만 지원을 해줘서 매우 가볍고, 개발자들이 일관성 있고 좋은 관습을 따르는 테스트 코드를 작성 할 수 있도록 유도해줍니다.
4 |
5 | ## 리액트 프로젝트 만들기
6 |
7 | > 이번에 만들 컴포넌트들은 Enzyme 편에서 만든 컴포넌트들과 똑같습니다. 단, Enzyme 부분을 생략하고 바로 여기로 넘어오시는 분들을 위하여 프로젝트를 새로구성하겠습니다.
8 |
9 | CRA 를 통하여 새 프로젝트를 만들어주세요.
10 |
11 | ```bash
12 | $ yarn create rtl-tutorial
13 | # 또는 npx create-react-app rtl-tutorial
14 | ```
15 |
16 | ## 설치
17 |
18 | react-testing-library 를 프로젝트에 설치해봅시다.
19 |
20 | ```bash
21 | $ yarn add react-testing-library jest-dom
22 | # 또는 npm install --save react-testing-library jest-dom
23 | ```
24 |
25 | > jest-dom 은 jest 확장으로서, DOM 에 관련된 `matcher` 를 추가해줍니다.
26 |
27 | > VS Code 를 사용하는 경우 @types/jest 패키지도 설치하세요.
28 |
29 | 그 다음, src 디렉터리에 setupTests.js 파일을 생성해서 다음 코드를 입력해주세요.
30 |
31 | #### `src/setupTests.js`
32 |
33 | ```javascript
34 | import 'react-testing-library/cleanup-after-each';
35 | import 'jest-dom/extend-expect';
36 | ```
37 |
38 | react-testing-library 에서는 리액트에서는 DOM 시뮬레이션을 위한 `[JSDOM](https://github.com/jsdom/jsdom)` 이라는 도구를 사용하여 `document.body` 에 리액트 컴포넌트를 렌더링합니다. `clean-up-after-each` 를 불러오면, 각 테스트 케이스가 끝날때마다 기존에 가상의 화면에 남아있는 UI 를 정리합니다.
39 |
40 | 추가적으로, 그 아래에는 `jest-dom/extend-expect` 를 불러와서 jest 에서 DOM 관련 `matcher` 를 사용 할 수 있게 해주었습니다.
41 |
42 | ## 첫번째 테스트 코드
43 |
44 | username 과 name 을 props 로 넣어주면 이를 렌더링해주는 Profile 컴포넌트를 만들어봅시다.
45 |
46 | #### `src/Profile.js`
47 |
48 | ```jsx
49 | import React from 'react';
50 |
51 | const Profile = ({ username, name }) => {
52 | return (
53 |
54 | {username}
55 | ({name})
56 |
57 | );
58 | };
59 |
60 | export default Profile;
61 | ```
62 |
63 | 이 컴포넌트를 만드셨으면 App 에서 렌더링해본 뒤 잘 보여지는지 먼저 확인해보세요.
64 |
65 | #### `src/App.js`
66 |
67 | ```jsx
68 | import React from 'react';
69 | import Profile from './Profile';
70 |
71 | const App = () => {
72 | return ;
73 | };
74 |
75 | export default App;
76 | ```
77 |
78 | App 을 수정하셨으면, `yarn start` (혹은 `npm start`) 를 입력하여 결과를 확인해보세요.
79 |
80 | 
81 |
82 | 그리고, Profile 컴포넌트를 위한 테스트 코드를 작성해봅시다.
83 |
84 | #### `src/Profile.test.js`
85 |
86 | ```jsx
87 | import React from 'react';
88 | import { render } from 'react-testing-library';
89 | import Profile from './Profile';
90 |
91 | describe('', () => {
92 | it('matches snapshot', () => {
93 | const utils = render();
94 | expect(utils.container).toMatchSnapshot();
95 | });
96 | it('shows the props correctly', () => {
97 | const utils = render();
98 | utils.getByText('velopert'); // velopert 라는 텍스트를 가진 엘리먼트가 있는지 확인
99 | utils.getByText('(김민준)'); // (김민준) 이라는 텍스트를 가진 엘리먼트가 있는지 확인
100 | utils.getByText(/김/); // 정규식 /김/ 을 통과하는 엘리먼트가 있는지 확인
101 | });
102 | });
103 | ```
104 |
105 | 이제 `yarn test` (혹은 `npm test`) 명령어를 실행해서 작성한 테스트가 잘 통과하는지 확인해보세요.
106 |
107 | react-testing-library 에서 컴포넌트를 렌더링 할 때에는 `render()` 라는 함수를 사용합니다. 이 함수가 호출되면 그 [결과물](https://testing-library.com/docs/react-testing-library/api#render-result) 에는 DOM 을 선택 할 수 있는 [다양한 쿼리](https://testing-library.com/docs/dom-testing-library/api-queries)들과 `container` 가 포함되어있는데요, 여기서 `container` 는 해당 컴포넌트의 최상위 `DOM` 을 가르킵니다. 이를 가지고 스냅샷 테스팅을 할 수도 있습니다.
108 |
109 | 그리고, 그 하단의 `getByText` 는 쿼리함수라고 부르는데요 이 함수를 사용하면 텍스트를 사용해서 원하는 DOM 을 선택 할 수 있습니다. 이에 대해서는 잠시 후 더 자세히 알아보겠습니다.
110 |
111 | ## 스냅샷 테스팅
112 |
113 | 스냅샷 테스팅이란, 렌더링된 결과가 이전에 렌더링한 결과와 일치하는지 확인하는 작업을 의미합니다.
114 |
115 | 코드를 저장하면 `src/__snapshots__/Profile.test.js.snap` 라는 파일이 다음과 같이 만들어질 것입니다.
116 |
117 | ```
118 | // Jest Snapshot v1, https://goo.gl/fbAQLP
119 |
120 | exports[` matches snapshot 1`] = `
121 |
134 | `;
135 | ```
136 |
137 | 컴포넌트가 렌더링됐을 때 이 스냅샷과 일치하지 않으면 테스트가 실패합니다. 만약에 스냅샷을 업데이트 하고싶다면 테스트가 실행되고 있는 콘솔 창에서 `u` 키를 누르면 됩니다.
138 |
139 | ## 다양한 쿼리
140 |
141 | `render` 함수를 실행하고 나면 그 결과물 안에는 [다양한 쿼리](https://testing-library.com/docs/dom-testing-library/api-queries) 함수들이 있는데요, 이 쿼리 함수들은 react-testing-library 의 기반인 [`dom-testing-library`](https://testing-library.com/docs/dom-testing-library/intro) 에서 지원하는 함수들입니다.
142 |
143 | 이 쿼리 함수들은 `Variant` 와 `Queries` 의 조합으로 네이밍이 이루어져있는데요, 우선 `Varient` 에는 어떤 종류들이 있는지 봅시다.
144 |
145 | ### Variant
146 |
147 | #### getBy
148 |
149 | `getBy*` 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나를 선택합니다. 만약에 없으면 에러가 발생합니다.
150 |
151 | #### getAllBy
152 |
153 | `getAllBy*` 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개를 선택합니다. 만약에 하나도 없으면 에러가 발생합니다.
154 |
155 | #### queryBy
156 |
157 | `queryBy*` 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나를 선택합니다. 만약에 존재하지 않아도 에러가 발생하지 않습니다.
158 |
159 | #### queryAllBy
160 |
161 | `queryAllBy*` 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개를 선택합니다. 만약에 존재하지 않아도 에러가 발생하지 않습니다.
162 |
163 | #### findBy
164 |
165 | `findBy*` 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나가 나타날 때 까지 기다렸다가 해당 DOM 을 선택하는 Promise 를 반환합니다. 기본 timeout 인 4500ms 이후에도 나타나지 않으면 에러가 발생합니다.
166 |
167 | #### findAllBy
168 |
169 | `findBy*` 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개가 나타날 때 까지 기다렸다가 해당 DOM 을 선택하는 Promise 를 반환합니다. 기본 timeout 인 4500ms 이후에도 나타나지 않으면 에러가 발생합니다.
170 |
171 | ### Queries
172 |
173 | #### ByLabelText
174 |
175 | `ByLabelText` 는 label 이 있는 input 의 label 내용으로 input 을 선택합니다.
176 |
177 | ```jsx
178 |
179 |
180 |
181 | const inputNode = getByLabelText('아이디');
182 | ```
183 |
184 | #### ByPlaceholderText
185 |
186 | `ByPlaceholderText` 는 placeholder 값으로 input 및 textarea 를 선택합니다.
187 |
188 | ```jsx
189 | ;
190 |
191 | const inputNode = getByPlaceholderText('아이디');
192 | ```
193 |
194 | #### ByText
195 |
196 | `ByText`는 엘리먼트가 가지고 있는 텍스트 값으로 DOM 을 선택합니다.
197 |
198 | ```jsx
199 |
Hello World!
;
200 |
201 | const div = getByText('Hello World!');
202 | ```
203 |
204 | 참고로, 텍스트 값에 정규식을 넣어도 작동합니다.
205 |
206 | ```jsx
207 | const div = getByText(/^Hello/);
208 | ```
209 |
210 | #### ByAltText
211 |
212 | `ByAltText` 는 `alt` 속성을 가지고 있는 엘리먼트 (주로 `img`) 를 선택합니다.
213 |
214 | ```jsx
215 | ;
216 |
217 | const imgAwesome = getByAltText('awesomse image');
218 | ```
219 |
220 | #### ByTitle
221 |
222 | `ByTitle` 은 `title` 속성을 가지고 있는 DOM 혹은 `title` 엘리먼트를 지니고있는 SVG 를 선택 할 때 사용합니다.
223 |
224 | > title 속성은 html 에서 툴팁을 보여줘야 하는 상황에 사용하곤 합니다.
225 |
226 | ```jsx
227 |
228 | 리액트는 짱 멋진 라이브러리다.
229 |
230 |
231 |
235 |
236 | const spanReact = getByTitle('React');
237 | const svgDelete = getByTitle('Delete');
238 | ```
239 |
240 | #### ByDisplayValue
241 |
242 | `ByDisplayValue` 는 `input`, `textarea`, `select` 가 지니고 있는 현재 값을 가지고 엘리먼트를 선택합니다.
243 |
244 | ```jsx
245 | ;
246 |
247 | const input = getByDisplayValue('text');
248 | ```
249 |
250 | #### ByRole
251 |
252 | `ByRole`은 특정 `role` 값을 지니고 있는 엘리먼트를 선택합니다.
253 |
254 | ```jsx
255 | 삭제;
256 |
257 | const spanRemove = getByRole('button');
258 | ```
259 |
260 | #### ByTestId
261 |
262 | `ByTestId` 는 다른 방법으로 못 선택할때 사용하는 방법인데요, 특정 DOM 에 직접 test 할 때 사용할 id 를 달아서 선택하는 것을 의미합니다.
263 |
264 | ```jsx
265 |
흔한 div
;
266 |
267 | const commonDiv = getByTestId('commondiv');
268 | ```
269 |
270 | !> **주의**: camelCase 가 아닙니다. 값을 설정할때 `data-testid="..."` 이렇게 설정하셔야합니다. 추가적으로, `ByTestId` 는 다른 방법으로 선택할 수 없을때에만 사용해야합니다.
271 |
272 | ### 어떤 쿼리를 사용해야 할까?
273 |
274 | 쿼리의 종류가 정말 많죠? 그렇다면, 어떤 쿼리를 우선적으로 사용해야 할까요? [매뉴얼](https://testing-library.com/docs/guide-which-query) 에서는 다음 우선순위를 따라서 사용하는것을 권장하고있습니다.
275 |
276 | 1. getByLabelText
277 | 2. getByPlaceholderText
278 | 3. getByText
279 | 4. getByDisplayValue
280 | 5. getByAltText
281 | 6. getByTitle
282 | 7. getByRole
283 | 8. getByTestId
284 |
285 | 그리고, DOM 의 `querySelector` 를 사용 할 수도 있는데요, 이는 지양해야합니다. 차라리 `data-testid` 를 설정하는것이 좋습니다.
286 |
287 | ```javascript
288 | const utils = render();
289 | const element = utils.container.querySelector('.my-class');
290 | ```
291 |
292 | ## Counter 컴포넌트 테스트 코드 작성하기
293 |
294 | 이번에는 Counter 컴포넌트를 만들고, 이를 위한 테스트 코드를 작성해봅시다.
295 |
296 | 먼저 Counter.js 파일을 생성해서 다음 코드를 작성하세요.
297 |
298 | #### `src/Counter.js`
299 |
300 | ```jsx
301 | import React, { useState, useCallback } from 'react';
302 |
303 | const Counter = () => {
304 | const [number, setNumber] = useState(0);
305 |
306 | const onIncrease = useCallback(() => {
307 | setNumber(number + 1);
308 | }, [number]);
309 |
310 | const onDecrease = useCallback(() => {
311 | setNumber(number - 1);
312 | }, [number]);
313 |
314 | return (
315 |
316 |
{number}
317 |
318 |
319 |
320 | );
321 | };
322 |
323 | export default Counter;
324 | ```
325 |
326 | 그리고, 이 컴포넌트를 App 에서 렌더링하여 잘 작동하는지 확인해보세요.
327 |
328 | ```jsx
329 | import React from 'react';
330 | import Counter from './Counter';
331 |
332 | const App = () => {
333 | return ;
334 | };
335 |
336 | export default App;
337 | ```
338 |
339 | 
340 |
341 | 잘 보여졌나요? 그럼 Counter를 위한 테스트 코드를 작성해보겠습니다.
342 |
343 | #### `src/Counter.test.js`
344 |
345 | ```jsx
346 | import React from 'react';
347 | import { render, fireEvent } from 'react-testing-library';
348 | import Counter from './Counter';
349 |
350 | describe('', () => {
351 | it('matches snapshot', () => {
352 | const utils = render();
353 | expect(utils.container).toMatchSnapshot();
354 | });
355 | it('has a number and two buttons', () => {
356 | const utils = render();
357 | // 버튼과 숫자가 있는지 확인
358 | utils.getByText('0');
359 | utils.getByText('+1');
360 | utils.getByText('-1');
361 | });
362 | it('increases', () => {
363 | const utils = render();
364 | const number = utils.getByText('0');
365 | const plusButton = utils.getByText('+1');
366 | // 클릭 이벤트를 두번 발생시키기
367 | fireEvent.click(plusButton);
368 | fireEvent.click(plusButton);
369 | expect(number).toHaveTextContent('2'); // jest-dom 의 확장 matcher 사용
370 | expect(number.textContent).toBe('2'); // textContent 를 직접 비교
371 | });
372 | it('decreases', () => {
373 | const utils = render();
374 | const number = utils.getByText('0');
375 | const plusButton = utils.getByText('-1');
376 | // 클릭 이벤트를 두번 발생시키기
377 | fireEvent.click(plusButton);
378 | fireEvent.click(plusButton);
379 | expect(number).toHaveTextContent('-2'); // jest-dom 의 확장 matcher 사용
380 | });
381 | });
382 | ```
383 |
384 | ### 이벤트 다루기
385 |
386 | 여기서 `fireEvent()` 라는 함수를 불러와서 사용했는데요, 이 함수는 이벤트를 발생시켜줍니다. 사용법은 다음과 같습니다.
387 |
388 | ```js
389 | fireEvent.이벤트이름(DOM, 이벤트객체);
390 | ```
391 |
392 | 클릭 이벤트의 경우엔 이벤트객체를 따로 넣어주지 않아도 되지만, 예를 들어서 change 이벤트의 경우엔 다음과 같이 해주어야합니다.
393 |
394 | ```js
395 | fireEvent.change(myInput, { target: { value: 'hello world' } });
396 | ```
397 |
398 | 이제 react-testing-library 의 주요 기능을 벌써 다 배우셨습니다!
399 |
400 | > 아직 다루지 않은 내용은 비동기 작업인데요, 이에 대한 내용은 나중에 이어질 섹션에서 다루게 됩니다.
401 |
--------------------------------------------------------------------------------
/docs/07-async-test.md:
--------------------------------------------------------------------------------
1 | # 7. 비동기 작업을 위한 테스트
2 |
3 | 리액트 애플리케이션에서 비동기 작업이 있을 때는 이를 어떻게 테스팅 하는지, 그리고 API 요청을 해야 하는 경우 이를 어떻게 mock 할 수 있는지에 대해서 알아보겠습니다.
4 |
5 | 우리가 이전에 만들었던 rtl-tutorial 프로젝트 디렉터리를 다시 에디터로 열어주세요.
6 |
7 | > 해당 프로젝트를 만들지 않았다면 [리액트 프로젝트 만들기](/05-react-testing-library?id=리액트-프로젝트-만들기) 부분을 참고하세요.
8 |
9 | ## 비동기적으로 바뀌는 컴포넌트 UI 테스트
10 |
11 | DelayedToggle 라는 컴포넌트를 만들어보세요.
12 |
13 | #### `src/DelayedToggle.js`
14 |
15 | ```jsx
16 | import React, { useState, useCallback } from 'react';
17 |
18 | const DelayedToggle = () => {
19 | const [toggle, setToggle] = useState(false);
20 | // 1초 후 toggle 값을 반전시키는 함수
21 | const onToggle = useCallback(() => {
22 | setTimeout(() => {
23 | setToggle(toggle => !toggle);
24 | }, 1000);
25 | }, []);
26 | return (
27 |
28 |
29 |
30 | 상태: {toggle ? 'ON' : 'OFF'}
31 |
32 | {toggle &&
야호!!
}
33 |
34 | );
35 | };
36 |
37 | export default DelayedToggle;
38 | ```
39 |
40 | 컴포넌트를 만드셨으면 App 에서 렌더링하고 `yarn start` 를 해서 브라우저에 띄운 후 버튼을 눌러보세요.
41 |
42 | #### `src/App.js`
43 |
44 | ```jsx
45 | import React from 'react';
46 | import DelayedToggle from './DelayedToggle';
47 |
48 | const App = () => {
49 | return ;
50 | };
51 |
52 | export default App;
53 | ```
54 |
55 | 이 컴포넌트는 다음과 같이 작동합니다.
56 |
57 | 
58 |
59 | 버튼이 클릭되면 1초후 상태 값이 바뀌고, 상태가 ON 일때는 "야호!!" 라는 텍스트가 보여집니다.
60 |
61 | 이런 컴포넌트의 테스트는 어떻게 작성 할 수 있는지 알아봅시다.
62 |
63 | ### Async Utilities
64 |
65 | 이런 테스트는 `react-testing-library` 에서 지원하는 [Async Utilities](https://testing-library.com/docs/dom-testing-library/api-async) 함수들을 사용하여 작성 할 수 있습니다.
66 |
67 | Aync Utilities 는 총 4가지 함수가 있는데요, 각 함수들을 직접 사용해보면서 사용법을 익혀봅시다.
68 |
69 | #### wait
70 |
71 | ```js
72 | function wait(
73 | callback?: () => void,
74 | options?: {
75 | timeout?: number
76 | interval?: number
77 | }
78 | ): Promise
79 | ```
80 |
81 | [`wait`](https://testing-library.com/docs/dom-testing-library/api-async#wait) 함수를 사용하면 특정 콜백에서 에러를 발생하지 않을 때 까지 대기할 수 있습니다. DelayedToggle 컴포넌트의 테스트 케이스를 다음과 같이 만들어보세요.
82 |
83 | #### `src/DelayedToggle.test.js`
84 |
85 | ```jsx
86 | import React from 'react';
87 | import DelayedToggle from './DelayedToggle';
88 | import {
89 | render,
90 | fireEvent,
91 | wait,
92 | waitForElement,
93 | waitForDomChange,
94 | waitForElementToBeRemoved
95 | } from 'react-testing-library';
96 |
97 | describe('', () => {
98 | it('reveals text when toggle is ON', async () => {
99 | const { getByText } = render();
100 | const toggleButton = getByText('토글');
101 | fireEvent.click(toggleButton);
102 | await wait(() => getByText('야호!!')); // 콜백 안의 함수가 에러를 발생시키지 않을 때 까지 기다립니다.
103 | });
104 | });
105 | ```
106 |
107 | `wait`함수는 콜백 안의 함수가 에러가 발생시키지 않을 때 까지 기다리다가, 대기시간이 timeout 을 초과하게 되면 테스트 케이스가 실패합니다. timeout 은 기본값 4500ms이며, 이는 다음과 같이 커스터마이징을 할 수 있습니다.
108 |
109 | ```js
110 | await wait(() => getByText('야호!!'), { timeout: 3000 }); // 콜백 안의 함수가 에러를 발생시키지 않을 때 까지 기다립니다.
111 | ```
112 |
113 | 이제 `yarn test` 를 입력하면 테스트가 진행이 될텐데, 리액트 16.8을 쓰고 계시다면 다음과 같은 경고가 발생 할 것입니다.
114 |
115 | ```
116 | ● Console
117 |
118 | console.error node_modules/react-dom/cjs/react-dom.development.js:506
119 | Warning: An update to DelayedToggle inside a test was not wrapped in act(...).
120 |
121 | When testing, code that causes React state updates should be wrapped into act(...):
122 |
123 | act(() => {
124 | /* fire events that update state */
125 | });
126 | /* assert on the output */
127 |
128 | This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
129 | in DelayedToggle (at DelayedToggle.test.js:14)
130 | ```
131 |
132 | 이는 리액트 16.9 에서는 고쳐지는 버그인데요, 아직 릴리즈되지는 않았습니다. 따라서, 이 경고를 숨기기 위하여 setupTests.js 파일을 다음과같이 수정해보세요.
133 |
134 | #### `src/setupTests.js`
135 |
136 | ```js
137 | import 'react-testing-library/cleanup-after-each';
138 | import 'jest-dom/extend-expect';
139 |
140 | // this is just a little hack to silence a warning that we'll get until react
141 | // fixes this: https://github.com/facebook/react/pull/14853
142 | const originalError = console.error;
143 | beforeAll(() => {
144 | console.error = (...args) => {
145 | if (/Warning.*not wrapped in act/.test(args[0])) {
146 | return;
147 | }
148 | originalError.call(console, ...args);
149 | };
150 | });
151 |
152 | afterAll(() => {
153 | console.error = originalError;
154 | });
155 | ```
156 |
157 | 작성 후 테스트 CLI 를 종료 후 다시 실행하세요.
158 |
159 | #### waitForElement
160 |
161 | ```js
162 | function waitForElement(
163 | callback: () => T,
164 | options?: {
165 | container?: HTMLElement
166 | timeout?: number
167 | mutationObserverOptions?: MutationObserverInit
168 | }
169 | ): Promise
170 | ```
171 |
172 | [`waitForElement`](https://testing-library.com/docs/dom-testing-library/api-async#waitforelement) 함수는 특정 엘리먼트가, 나타났거나, 바뀌었거나, 사라질때까지 대기를 해줍니다. 그리고 프로미스가 끝날 때 우리가 선택한 엘리먼트를 resolve 합니다.
173 |
174 | DelayedToggle 컴포넌트의 텍스트가 바뀌는 것을 검증하는 테스트 케이스를 `waitForElement` 로 한번 구현을 해보겠습니다.
175 |
176 | #### `src/DelayedToggle.test.js`
177 |
178 | ```jsx
179 | import React from 'react';
180 | import DelayedToggle from './DelayedToggle';
181 | import {
182 | render,
183 | fireEvent,
184 | wait,
185 | waitForElement,
186 | waitForDomChange,
187 | waitForElementToBeRemoved
188 | } from 'react-testing-library';
189 |
190 | describe('', () => {
191 | it('reveals text when toggle is ON', async () => {
192 | const { getByText } = render();
193 | const toggleButton = getByText('토글');
194 | fireEvent.click(toggleButton);
195 | await wait(() => getByText('야호!!')); // 콜백 안의 함수가 에러를 발생시키지 않을 때 까지 기다립니다.
196 | });
197 |
198 | it('toggles text ON/OFF', async () => {
199 | const { getByText } = render();
200 | const toggleButton = getByText('토글');
201 | fireEvent.click(toggleButton);
202 | const text = await waitForElement(() => getByText('ON'));
203 | expect(text).toHaveTextContent('ON');
204 | });
205 | });
206 | ```
207 |
208 | #### waitForDomChange
209 |
210 | ```js
211 | function waitForDomChange(options?: {
212 | container?: HTMLElement
213 | timeout?: number
214 | mutationObserverOptions?: MutationObserverInit
215 | }): Promise
216 | ```
217 |
218 | [`waitForDomChange`](https://testing-library.com/docs/dom-testing-library/api-async#waitfordomchange)의 특징은, 콜백함수가 아니라 검사하고 싶은 엘리먼트를 넣어주면 해당 엘리먼트에서 변화가 발생 할 때 까지 기다려준다는 것 입니다. 우리가 `render` 를 했을때 결과값에 있는 `container` 를 넣어주면, 사전에 쿼리를 통하여 엘리먼트를 선택하지 않아도 변화가 발생했음을 감지할 수 있습니다. 또한, 프로미스가 resolve 됐을 땐 [`mutationList`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/MutationObserver) 를 반환하여 DOM이 어떻게 바뀌었는지에 대한 정보를 알수있습니다.
219 |
220 | #### `src/DelayedToggle.test.js`
221 |
222 | ```jsx
223 | import React from 'react';
224 | import DelayedToggle from './DelayedToggle';
225 | import {
226 | render,
227 | fireEvent,
228 | wait,
229 | waitForElement,
230 | waitForDomChange,
231 | waitForElementToBeRemoved
232 | } from 'react-testing-library';
233 |
234 | describe('', () => {
235 | it('reveals text when toggle is ON', async () => {
236 | const { getByText } = render();
237 | const toggleButton = getByText('토글');
238 | fireEvent.click(toggleButton);
239 | await wait(() => getByText('야호!!')); // 콜백 안의 함수가 에러를 발생시키지 않을 때 까지 기다립니다.
240 | });
241 |
242 | it('toggles text ON/OFF', async () => {
243 | const { getByText } = render();
244 | const toggleButton = getByText('토글');
245 | fireEvent.click(toggleButton);
246 | const text = await waitForElement(() => getByText('ON'));
247 | expect(text).toHaveTextContent('ON');
248 | });
249 |
250 | it('changes something when button is clicked', async () => {
251 | const { getByText, container } = render();
252 | const toggleButton = getByText('토글');
253 | fireEvent.click(toggleButton);
254 | const mutations = await waitForDomChange({ container });
255 | console.log(mutations);
256 | });
257 | });
258 | ```
259 |
260 | #### waitForElementToBeRemoved
261 |
262 | ```js
263 | function waitForElementToBeRemoved(
264 | callback: () => T,
265 | options?: {
266 | container?: HTMLElement
267 | timeout?: number
268 | mutationObserverOptions?: MutationObserverInit
269 | }
270 | ): Promise
271 | ```
272 |
273 | [`waitForElementToBeRemove`](https://testing-library.com/docs/dom-testing-library/api-async#waitforelementtoberemoved)는 특정 엘리먼트가 화면에서 사라질때까지 기다리는 함수입니다.
274 |
275 | #### `src/DelayedToggle.test.js`
276 |
277 | ```jsx
278 | import React from 'react';
279 | import DelayedToggle from './DelayedToggle';
280 | import {
281 | render,
282 | fireEvent,
283 | wait,
284 | waitForElement,
285 | waitForDomChange,
286 | waitForElementToBeRemoved
287 | } from 'react-testing-library';
288 |
289 | describe('', () => {
290 | it('reveals text when toggle is ON', async () => {
291 | const { getByText } = render();
292 | const toggleButton = getByText('토글');
293 | fireEvent.click(toggleButton);
294 | await wait(() => getByText('야호!!')); // 콜백 안의 함수가 에러를 발생시키지 않을 때 까지 기다립니다.
295 | });
296 |
297 | it('toggles text ON/OFF', async () => {
298 | const { getByText } = render();
299 | const toggleButton = getByText('토글');
300 | fireEvent.click(toggleButton);
301 | const text = await waitForElement(() => getByText('ON'));
302 | expect(text).toHaveTextContent('ON');
303 | });
304 |
305 | it('changes something when button is clicked', async () => {
306 | const { getByText, container } = render();
307 | const toggleButton = getByText('토글');
308 | fireEvent.click(toggleButton);
309 | const mutations = await waitForDomChange({ container });
310 | });
311 |
312 | it('removes text when toggle is OFF', async () => {
313 | const { getByText, container } = render();
314 | const toggleButton = getByText('토글');
315 | fireEvent.click(toggleButton);
316 | await waitForDomChange({ container }); // ON 이 됨
317 | getByText('야호!!');
318 | fireEvent.click(toggleButton);
319 | await waitForElementToBeRemoved(() => getByText('야호!!'));
320 | });
321 | });
322 | ```
323 |
324 | 이제, 컴포넌트의 UI 가 비동기적으로 바뀔 때 어떻게 처리해야 되는지 잘 알겠지요?
325 |
326 | ## REST API 호출하는 경우의 테스트
327 |
328 | 이번에는 리액트 컴포넌트에서 REST API 를 연동하는 경우 어떻게 테스트를 해야하는지 알아봅시다.
329 |
330 | 테스트 할 컴포넌트를 먼저 만들어봅시다!
331 |
332 | 우선 HTTP Client 라이브러리인 axios 를 설치하세요.
333 |
334 | ```
335 | $ yarn add axios
336 | ```
337 |
338 | 우리는 [JSONPlaceholder](https://jsonplaceholder.typicode.com/) 에서 제공하는 가짜 API 를 사용하겠습니다.
339 |
340 | #### API 예시
341 |
342 | ```
343 | GET https://jsonplaceholder.typicode.com/users/1
344 |
345 | {
346 | "id": 1,
347 | "name": "Leanne Graham",
348 | "username": "Bret",
349 | "email": "Sincere@april.biz",
350 | "address": {
351 | "street": "Kulas Light",
352 | "suite": "Apt. 556",
353 | "city": "Gwenborough",
354 | "zipcode": "92998-3874",
355 | "geo": {
356 | "lat": "-37.3159",
357 | "lng": "81.1496"
358 | }
359 | },
360 | "phone": "1-770-736-8031 x56442",
361 | "website": "hildegard.org",
362 | "company": {
363 | "name": "Romaguera-Crona",
364 | "catchPhrase": "Multi-layered client-server neural-net",
365 | "bs": "harness real-time e-markets"
366 | }
367 | }
368 | ```
369 |
370 | ### 예제 컴포넌트 만들기
371 |
372 | id 값을 props 로 받아오면, 위 API 를 호출하고 결과에서 username 과 email 을 보여주는 컴포넌트를 만들어봅시다.
373 |
374 | #### `src/UserProfile.js`
375 |
376 | ```jsx
377 | import React, { useEffect, useState } from 'react';
378 | import axios from 'axios';
379 |
380 | const UserProfile = ({ id }) => {
381 | const [userData, setUserData] = useState(null);
382 | const [loading, setLoading] = useState(false);
383 | const getUser = async id => {
384 | setLoading(true);
385 | try {
386 | const response = await axios.get(
387 | `https://jsonplaceholder.typicode.com/users/${id}`
388 | );
389 | setUserData(response.data);
390 | } catch (e) {
391 | console.log(e);
392 | }
393 | setLoading(false);
394 | };
395 | useEffect(() => {
396 | getUser(id);
397 | }, [id]);
398 |
399 | if (loading) return
415 | );
416 | };
417 |
418 | export default UserProfile;
419 | ```
420 |
421 | 컴포넌트를 만드셨으면, App 에서 렌더링해서 이 컴포넌트가 어떻게 작동하는지 확인해보세요.
422 |
423 | 
424 |
425 | 이렇게 REST API 를 호출해야 하는 컴포넌트의 경우, 테스트 코드에서도 똑같이 요청을 보낼 수는 있지만, 일반적으로 서버에 API 를 직접 호출하지는 않고 이를 mocking 합니다. 왜냐하면, 서버의 API 가 실제로 작동하고 안하고는 서버쪽의 일이기 때문이기 때문입니다.
426 |
427 | 때문에, axios 를 사용했을 때 실제로 요청이 발생하지는 않지만 마치 발생한것처럼 작동하게 하는 방법이 있는데요, 대표적으로 두가지가 있는데 [node_modules 를 mocking](https://www.leighhalliday.com/mocking-axios-in-jest-testing-async-functions) 하는 방법이 있고, [axios-mock-adapter](https://www.npmjs.com/package/axios-mock-adapter) 라는 라이브러리를 쓰는 방법이 있습니다.
428 |
429 | 우리는 `axios-mock-adapter` 를 사용하겠습니다. 라이브러리를 사용하는편이 준비해야 할 코드도 적고 훨씬 편리합니다.
430 |
431 | ### axios-mock-adapter 사용해보기
432 |
433 | UserProfile 의 테스트 코드를 다음과 같이 작성해보세요.
434 |
435 | #### `src/UserProfile.test.js`
436 |
437 | ```jsx
438 | import React from 'react';
439 | import { render } from 'react-testing-library';
440 | import UserProfile from './UserProfile';
441 | import axios from 'axios';
442 | import MockAdapter from 'axios-mock-adapter';
443 |
444 | describe('', () => {
445 | const mock = new MockAdapter(axios, { delayResponse: 200 }); // 200ms 가짜 딜레이 설정
446 | // API 요청에 대하여 응답 미리 정하기
447 | mock.onGet('https://jsonplaceholder.typicode.com/users/1').reply(200, {
448 | id: 1,
449 | name: 'Leanne Graham',
450 | username: 'Bret',
451 | email: 'Sincere@april.biz',
452 | address: {
453 | street: 'Kulas Light',
454 | suite: 'Apt. 556',
455 | city: 'Gwenborough',
456 | zipcode: '92998-3874',
457 | geo: {
458 | lat: '-37.3159',
459 | lng: '81.1496'
460 | }
461 | },
462 | phone: '1-770-736-8031 x56442',
463 | website: 'hildegard.org',
464 | company: {
465 | name: 'Romaguera-Crona',
466 | catchPhrase: 'Multi-layered client-server neural-net',
467 | bs: 'harness real-time e-markets'
468 | }
469 | });
470 | it('loads userData properly', () => {
471 | // TODO
472 | });
473 | });
474 | ```
475 |
476 | `MockAdapter` 를 사용하면 특정 API 요청이 발생했을 때 어떤 응답이 와야 하는지 직접 정의해줄 수 있습니다. 그러면, 컴포넌트 내부에서 API 요청이 발생하게 될 때, 실제로 서버까지 요청이 날라가지 않고, 우리가 정의한 가짜 응답을 사용하게 됩니다.
477 |
478 | MockAdapter 를 사용 할 때는 `delayResponse` 옵션을 설정하면 딜레이를 임의적으로 설정할 수 있습니다. 이 설정은 없어도 상관 없습니다.
479 |
480 | 이렇게 axios 요청을 mocking 한 이후에는 우리가 이전에 배웠던 Async Utilities 를 사용해주면 됩니다.
481 |
482 | #### `src/UserProfile.test.js`
483 |
484 | ```jsx
485 | import React from 'react';
486 | import { render, waitForElement } from 'react-testing-library';
487 | import UserProfile from './UserProfile';
488 | import axios from 'axios';
489 | import MockAdapter from 'axios-mock-adapter';
490 |
491 | describe('', () => {
492 | const mock = new MockAdapter(axios, { delayResponse: 200 }); // 200ms 가짜 딜레이 설정
493 | // API 요청에 대하여 응답 미리 정하기
494 | mock.onGet('https://jsonplaceholder.typicode.com/users/1').reply(200, {
495 | id: 1,
496 | name: 'Leanne Graham',
497 | username: 'Bret',
498 | email: 'Sincere@april.biz',
499 | address: {
500 | street: 'Kulas Light',
501 | suite: 'Apt. 556',
502 | city: 'Gwenborough',
503 | zipcode: '92998-3874',
504 | geo: {
505 | lat: '-37.3159',
506 | lng: '81.1496'
507 | }
508 | },
509 | phone: '1-770-736-8031 x56442',
510 | website: 'hildegard.org',
511 | company: {
512 | name: 'Romaguera-Crona',
513 | catchPhrase: 'Multi-layered client-server neural-net',
514 | bs: 'harness real-time e-markets'
515 | }
516 | });
517 | it('calls getUser API loads userData properly', async () => {
518 | const { getByText } = render();
519 | await waitForElement(() => getByText('로딩중..')); // 로딩중.. 문구 보여줘야함
520 | await waitForElement(() => getByText('Bret')); // Bret (username) 을 보여줘야함
521 | });
522 | });
523 | ```
524 |
525 | 테스트가 잘 통과했나요?
526 |
527 | ### axios-mock-adapter 활용방법
528 |
529 | `axios-mock-adapter` 의 [공식 문서](https://www.npmjs.com/package/axios-mock-adapter)를 보면 더 많은 활용방법을 볼 수 있는데요, 그 중 일부를 어떤 용도로 사용 할 수 있는지 소개시켜드리겠습니다.
530 |
531 | #### 한번만 mocking 하기 - replyOnce
532 |
533 | ```js
534 | mock.onGet('/users').replyOnce(200, users);
535 | ```
536 |
537 | 이렇게 하면 요청을 딱 한번만 mocking 할 수 있습니다. 한번 요청을 하고 나면 그 다음 요청은 정상적으로 요청이 됩니다.
538 |
539 | #### replyOnce 를 연달아서 사용하기
540 |
541 | ```js
542 | mock
543 | .onGet('/users')
544 | .replyOnce(200, users) // 첫번째 요청
545 | .onGet('/users')
546 | .replyOnce(500); // 두번째 요청
547 | ```
548 |
549 | 이렇게 하면 첫번째 요청과 두번째 요청을 연달아서 설정 할 수 있습니다. 요청을 여러번 해야 하는 경우 이런 형태로 구현하시면 됩니다.
550 |
551 | #### 아무 요청이나 mocking 하기 - onAny()
552 |
553 | 보통 메서드에 따라 `onGet()`, `onPost()` 이런식으로 사용하는데요, `onAny()` 를 사용하면 어떤 메서드던 mocking 을 할 수 있습니다.
554 |
555 | ```js
556 | mock.onAny('/foo').reply(200);
557 | ```
558 |
559 | 만약에 주소까지 생략하면 어떤 주소던 mocking 합니다.
560 |
561 | ```js
562 | mock.onAny().reply(200);
563 | ```
564 |
565 | #### reset 과 restore
566 |
567 | mock 인스턴스에는 `reset` 과 `restore` 라는 함수가 있습니다.
568 |
569 | ```js
570 | mock.reset();
571 | ```
572 |
573 | `reset` 은 mock 인스턴스에 등록된 모든 mock 핸들러를 제거합니다. 만약에 테스트 케이스별로 다른 mock 설정을 하고 싶으시면 이 함수를 사용하시면 됩니다.
574 |
575 | ```js
576 | mock.restore();
577 | ```
578 |
579 | `restore` 은 axios 에서 mocking 기능을 완전히 제거합니다. 만약에 실제 테스트를 하다가 요청이 실제로 날라가게 하고 싶으면 이 함수를 사용하면 됩니다.
580 |
--------------------------------------------------------------------------------
/docs/08-redux-test.md:
--------------------------------------------------------------------------------
1 | # 리덕스를 사용하는 리액트 프로젝트 테스트
2 |
3 | 이번에는 리덕스를 사용하는 리액트 프로젝트에서는 어떻게 테스트를 작성하는지 배워봅시다.
4 |
5 | > 만약 리덕스를 잘 모르신다면 이 [링크](https://velog.io/@velopert/Redux-1-%EC%86%8C%EA%B0%9C-%EB%B0%8F-%EA%B0%9C%EB%85%90%EC%A0%95%EB%A6%AC-zxjlta8ywt) 를 통하여 리덕스를 배우신 뒤 이 튜토리얼을 진행해주세요.
6 |
7 | 리덕스를 사용 할 때 테스트 해야 되는 것들은 다음과 같습니다.
8 |
9 | 1. 액션 생성 함수
10 | 2. 리듀서
11 | 3. 프리젠테이셔널(Presentational) 컴포넌트
12 | 4. 컨테이너(Container) 컴포넌트
13 |
14 | > 여기서 프리젠테이셔널 컴포넌트는 리덕스에 연결되지 않은 컴포넌트를 의미하고, 컨테이너 컴포넌트는 리덕스에 연결된 컴포넌트를 의미합니다.
15 |
16 | 1번과 2번은 별도의 라이브러리 도움 없이 테스트 코드를 작성하시면 되고, 3번과 4번의 경우엔 react-testing-library 를 사용하면 됩니다. 4번, 컨테이너 컴포넌트를 위한 테스트를 작성하는건 꽤나 간단합니다. 리덕스가 연결되어있다고 해서 크게 어려워지는 것은 없습니다. 그냥 컴포넌트에 리덕스 스토어를 연결해주기만 하면 됩니다.
17 |
18 | ## 프로젝트 새로 만들기
19 |
20 | 리덕스를 사용하는 리액트 프로젝트를 위한 테스트 코드를 작성해보기 전에 우선 샘플 리덕스 코드를 준비해주겠습니다. 기존의 rtl-tutorial 프로젝트에는 연습용 컴포넌트들이 너무 많아졌으니, 새로운 프로젝트를 만들어주세요.
21 |
22 | ```bash
23 | $ yarn create react-app redux-test-tutorial
24 | ```
25 |
26 | 그리고, 해당 디렉터리에 들어가서 테스트 관련 라이브러리와 리덕스 관련 라이브러리들을 설치하세요.
27 |
28 | ```bash
29 | $ yarn add react-testing-library jest-dom redux react-redux
30 | ```
31 |
32 | 다음, src 디렉터리에 `setupTests.js` 도 만들어주세요.
33 |
34 | #### `src/setupTests.js`
35 |
36 | ```javascript
37 | import 'react-testing-library/cleanup-after-each';
38 | import 'jest-dom/extend-expect';
39 | ```
40 |
41 | 이제 프로젝트에 리덕스를 적용해주겠습니다. 우리는 [Ducks 패턴](https://github.com/JisuPark/ducks-modular-redux)을 사용해서 리덕스 모듈을 작성 할 건데요, 지금 당장은 주요 리덕스 관련 코드를 작성하지는 않고 파일만 만들어주겠습니다.
42 |
43 | > 이 부분은 현재 작성중입니다..! 강의에서는 코드로 설명합니다 ;)
44 |
45 | [](https://codesandbox.io/s/reduxtesttutorial-0lbut?fontsize=14)
46 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # 벨로퍼트와 함께하는 리액트 테스팅
2 |
3 | 이 튜토리얼에서는 리액트 프로젝트에서 TDD (Test Driven Development · 테스트 주도 개발)을 하는 방법에 대해서 알아보겠습니다. 소프트웨어 개발에서의 "테스트" 라는 개념에 대해서 1도 모르는 개발자도 이 튜토리얼을 마치고나면, "나는 리액트 테스팅을 잘 할 수 있다!" 라고 말 할 수 있습니다.
4 |
5 | > 이 튜토리얼을 진행하기 전에는 [Node.js](https://nodejs.org/ko/) 와 [yarn](https://yarnpkg.com/lang/en/) 이 설치되어있어야 합니다. yarn 을 사용하는것을 권장드리지만, yarn 을 싫어하신다면 npm 을 쓰셔도 상관없습니다.
6 |
7 | ## 테스트란?
8 |
9 | 테스트란, 우리가 작성한 코드가 잘 작동한다는 것을 검증하는 작업을 의미합니다. 특정 기능이 잘 작동하는지 확인하려면 우리는 어떻게 해야할까요? 가장 기본적인 방법으로는 우리가 구현한 기능을 직접 사용해보는 것입니다. 예를 들어서 직접 마우스로 눌러보고, 키보드로 입력해서 우리의 의도대로 잘 작동하는건지 확인하는 것이죠.
10 | 그런데 우리가 만든 프로젝트의 모든 기능을 사람이 수동으로 하나하나 확인하는 것은 정말 번거로운 일 입니다. 그래서 우리는 테스트 자동화라는 작업을 합니다. 사람이 직접 확인을 하는 것이 아니라 테스트를 하는 코드를 작성해서, 테스트 시스템이 자동으로 확인을 해줄 수 있게 하는 것이죠. 이를 테스트 자동화라고 합니다.
11 |
12 | ## 테스트 자동화를 통해 얻을 수 있는 이점
13 |
14 | 테스트 자동화를 함으로서 어떤 이점을 얻을 수 있을까요? 만약 여러분들이 프로젝트를 다른 사람들과 협업을 하게 되는 경우 테스트 코드를 작성하는 것은 매우 큰 도움을 줍니다. 예를들어 여러분이 코드 A 를 작성하고, 다른 개발자가 코드 B 를 작성했다고 가정해봅시다. 어느날 여러분이 코드를 수정하면서 A 코드와 B 코드를 조금씩 수정했는데, 여러분이 직접 확인 할 때는 모두 잘 작동하는지 알았는데, 알고보니 사소한 실수를 했다거나 어떠한 상황을 고려하지 못해서 B 코드의 기능의 일부가 고장나있을 수도 있습니다. 그런데 그걸 캐치하지 못하고 서비스가 배포되어 치명적인 버그가 있는 상태로 사용자에게 제공되었다면? 매우 안타까운 일이겠죠? 만약에 프로젝트의 규모가 정말 커서 고려해야 할 사항이 많거나, 여러명이 작업을 진행 한 코드여서 코드를 수정한 사람이 해당 코드를 모두 제대로 파악하고 있지 않을 때 이러한 일이 발생하기 쉽습니다. 그런데 만약 우리가 테스트 자동화를 했더라면 우리가 준비해놓은 상황에 대하여 자동으로 빠르게 검사를 해줄 수 있기 때문에 코드가 이전과 똑같이 작동하는지 아니면 고장났는지 쉽게 판단을 할 수 있어서 이러한 안타까운 일을 방지 할 수 있습니다.
15 |
16 | 테스트 코드를 사용하면 우리가 프로젝트를 개발하는 과정에서 우리가 써내려가는 코드가 기존의 기능들을 실수로 망가뜨리는것을 아주 효과적으로 방지 할 수 있습니다. 또한 개발하게 될 떄 실제 발생 할 수 있게 되는 상황에 대하여 미리 정리해놓고 그에 맞춰 코드를 작성하게 되면 우리가 실수로 빠뜨릴 수 있는 사항들을 까먹지 않고 잘 챙길 수 있게 됩니다.
17 |
18 | 이 뿐만이 아닙니다. 코드를 리팩토링 할 때 정말 좋습니다. 예를 들어서 우리가 A 라는 기능을 리팩토링한다고 가정해봅시다. 리팩토링을 하고 있는 기능이 규모가 커다란 기능이라면 실수로 빠뜨릴 수 있는 사항도 있을 수 있습니다. 그래서, 리팩토링 이후에는 버그가 있는지 없는지 정말 세밀하게 또 확인을 해봐야하죠. 그런데? 테스트 코드가 존재한다면, 리팩토링 이후에 코드가 이전과 똑같이 작동하는지 검증하는게 매우 쉬워지기 때문에 코드의 질을 향상시키는 것에 매우 큰 도움이 됩니다.
19 |
20 | 테스트 코드를 작성한다고해서 프로젝트에서 버그가 발생하지 않는 것은 아닙니다. 테스트 코드를 작성해도, 프로젝트에는 버그가 발생할 수 있습니다. 하지만, 만약 버그가 발생했더라면, 그 버그를 고치고 나서, 버그가 발생하는 상황에 대한 테스트 코드를 작성해두면, 두번 다시 똑같은 실수를 하는 것을 방지할 수 있습니다.
21 |
22 | ## 유닛 테스트와 통합 테스트
23 |
24 | 테스트 코드는 크게 두 종류로 나뉘어질 수 있습니다.
25 |
26 | ### 유닛(Unit) 테스트
27 |
28 | 첫번째는 유닛 테스트입니다. 유닛 테스트는 아주 조그마한 단위로 작성됩니다. 한번 유닛 테스트의 예시들을 확인해볼까요?
29 |
30 | - 컴포넌트가 잘 렌더링된다.
31 | - 컴포넌트의 특정 함수를 실행하면 상태가 우리가 원하는 형태로 바뀐다
32 | - 리덕스의 액션 생성 함수가 액션 객체를 잘 만들어낸다
33 | - 리덕스의 리듀서에 상태와 액션객체를 넣어서 호출하면 새로운 상태를 잘 만들어준다.
34 |
35 | 프로젝트의 기능을 잘게잘게 쪼개서 테스트를 하면 각 기능이 모두 잘 작동하는지 확인 할 수는 있습니다. 그런데, 전체적으로 잘 작동하는지 확인이 잘 안될 수도 있습니다.
36 |
37 | 
38 |
39 | 
40 |
41 | 
42 |
43 | ### 통합(Integrated) 테스트
44 |
45 | 기능들이 전체적으로 잘 작동하는지 확인하기 위해서 사용 하는 것이 바로 통합 테스트입니다. 통합 테스트에 대한 예시도 확인해볼까요?
46 |
47 | - 여러 컴포넌트들을 렌더링하고 서로 상호 작용을 잘 하고 있다
48 | - DOM 이벤트를 발생 시켰을 때 우리의 UI 에 원하는 변화가 잘 발생한다
49 | - 리덕스와 연동된 컨테이너 컴포넌트의 DOM 에 특정 이벤트를 발생시켰을 때 우리가 원하는 액션이 잘 디스패치 된다
50 |
51 | 유닛 테스트와 통합 테스트 간의 차이는 간단합니다. 유닛 테스트는 하나에 초점을 둔다면, 통합 테스트는 이름이 그러하듯 여러 요소들을 고려하여 작성합니다. 유닛 테스트는 보통 한 파일만 불러와서 진행하는 반면, 통합 테스트는 여러 요소들을 고려하는 과정에서, 여러 파일들을 불러와서 사용하게 될 수도 있습니다. 추가적으로, 한 파일에 있는 여러 기능들을 함께 사용하는 것도 통합테스트로 간주됩니다.
52 |
--------------------------------------------------------------------------------
/docs/_sidebar.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | - [벨로퍼트와 함께하는 리액트 테스팅](/)
4 | - [1. 자바스크립트 테스팅의 기초](01-javascript-testing.md)
5 | - [2. TDD의 소개](02-tdd-introduction.md)
6 | - [3. 리액트 컴포넌트의 테스트](03-react-test.md)
7 | - [4. Enzyme 사용법](04-enzyme.md)
8 | - [5. React-testing-library 사용법](05-react-testing-library.md)
9 | - [6. TDD 개발 흐름으로 투두리스트 만들기](06-rtl-tdd.md)
10 | - [7. 비동기 작업을 위한 테스트](07-async-test.md)
11 | - [8. 리덕스를 사용하는 리액트 프로젝트 테스트](08-redux-test.md)
12 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 벨로퍼트와 함께하는 리액트 테스팅
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/docs/reference.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/velopert/learn-react-testing/6c006c222684ac0048c3dcb14fc4bf3bdaf77c3b/docs/reference.md
--------------------------------------------------------------------------------
/javascript/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 |
4 | module.exports = {
5 | // All imported modules in your tests should be mocked automatically
6 | // automock: false,
7 |
8 | // Stop running tests after `n` failures
9 | // bail: 0,
10 |
11 | // Respect "browser" field in package.json when resolving modules
12 | // browser: false,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "/private/var/folders/7c/_wgf_n092337x7g32fgtrghc0000gn/T/jest_dx",
16 |
17 | // Automatically clear mock calls and instances between every test
18 | clearMocks: true,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | // collectCoverage: false,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | // collectCoverageFrom: null,
25 |
26 | // The directory where Jest should output its coverage files
27 | coverageDirectory: "coverage",
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | // coveragePathIgnorePatterns: [
31 | // "/node_modules/"
32 | // ],
33 |
34 | // A list of reporter names that Jest uses when writing coverage reports
35 | // coverageReporters: [
36 | // "json",
37 | // "text",
38 | // "lcov",
39 | // "clover"
40 | // ],
41 |
42 | // An object that configures minimum threshold enforcement for coverage results
43 | // coverageThreshold: null,
44 |
45 | // A path to a custom dependency extractor
46 | // dependencyExtractor: null,
47 |
48 | // Make calling deprecated APIs throw helpful error messages
49 | // errorOnDeprecated: false,
50 |
51 | // Force coverage collection from ignored files using an array of glob patterns
52 | // forceCoverageMatch: [],
53 |
54 | // A path to a module which exports an async function that is triggered once before all test suites
55 | // globalSetup: null,
56 |
57 | // A path to a module which exports an async function that is triggered once after all test suites
58 | // globalTeardown: null,
59 |
60 | // A set of global variables that need to be available in all test environments
61 | // globals: {},
62 |
63 | // An array of directory names to be searched recursively up from the requiring module's location
64 | // moduleDirectories: [
65 | // "node_modules"
66 | // ],
67 |
68 | // An array of file extensions your modules use
69 | // moduleFileExtensions: [
70 | // "js",
71 | // "json",
72 | // "jsx",
73 | // "ts",
74 | // "tsx",
75 | // "node"
76 | // ],
77 |
78 | // A map from regular expressions to module names that allow to stub out resources with a single module
79 | // moduleNameMapper: {},
80 |
81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
82 | // modulePathIgnorePatterns: [],
83 |
84 | // Activates notifications for test results
85 | // notify: false,
86 |
87 | // An enum that specifies notification mode. Requires { notify: true }
88 | // notifyMode: "failure-change",
89 |
90 | // A preset that is used as a base for Jest's configuration
91 | // preset: null,
92 |
93 | // Run tests from one or more projects
94 | // projects: null,
95 |
96 | // Use this configuration option to add custom reporters to Jest
97 | // reporters: undefined,
98 |
99 | // Automatically reset mock state between every test
100 | // resetMocks: false,
101 |
102 | // Reset the module registry before running each individual test
103 | // resetModules: false,
104 |
105 | // A path to a custom resolver
106 | // resolver: null,
107 |
108 | // Automatically restore mock state between every test
109 | // restoreMocks: false,
110 |
111 | // The root directory that Jest should scan for tests and modules within
112 | // rootDir: null,
113 |
114 | // A list of paths to directories that Jest should use to search for files in
115 | // roots: [
116 | // ""
117 | // ],
118 |
119 | // Allows you to use a custom runner instead of Jest's default test runner
120 | // runner: "jest-runner",
121 |
122 | // The paths to modules that run some code to configure or set up the testing environment before each test
123 | // setupFiles: [],
124 |
125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
126 | // setupFilesAfterEnv: [],
127 |
128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
129 | // snapshotSerializers: [],
130 |
131 | // The test environment that will be used for testing
132 | testEnvironment: "node",
133 |
134 | // Options that will be passed to the testEnvironment
135 | // testEnvironmentOptions: {},
136 |
137 | // Adds a location field to test results
138 | // testLocationInResults: false,
139 |
140 | // The glob patterns Jest uses to detect test files
141 | // testMatch: [
142 | // "**/__tests__/**/*.[jt]s?(x)",
143 | // "**/?(*.)+(spec|test).[tj]s?(x)"
144 | // ],
145 |
146 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
147 | // testPathIgnorePatterns: [
148 | // "/node_modules/"
149 | // ],
150 |
151 | // The regexp pattern or array of patterns that Jest uses to detect test files
152 | // testRegex: [],
153 |
154 | // This option allows the use of a custom results processor
155 | // testResultsProcessor: null,
156 |
157 | // This option allows use of a custom test runner
158 | // testRunner: "jasmine2",
159 |
160 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
161 | // testURL: "http://localhost",
162 |
163 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
164 | // timers: "real",
165 |
166 | // A map from regular expressions to paths to transformers
167 | // transform: null,
168 |
169 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
170 | // transformIgnorePatterns: [
171 | // "/node_modules/"
172 | // ],
173 |
174 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
175 | // unmockedModulePathPatterns: undefined,
176 |
177 | // Indicates whether each individual test should be reported during the run
178 | // verbose: null,
179 |
180 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
181 | // watchPathIgnorePatterns: [],
182 |
183 | // Whether to use watchman for file crawling
184 | // watchman: true,
185 | };
186 |
--------------------------------------------------------------------------------
/javascript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "javascript",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "@types/jest": "^24.0.13",
8 | "jest": "^24.8.0"
9 | },
10 | "scripts": {
11 | "test": "jest --watchAll --verbose"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/javascript/stats.js:
--------------------------------------------------------------------------------
1 | exports.max = numbers => Math.max(...numbers);
2 | exports.min = numbers => Math.min(...numbers);
3 | exports.avg = numbers =>
4 | numbers.reduce(
5 | (acc, current, index, { length }) => acc + current / length,
6 | 0
7 | );
8 |
9 | exports.sort = numbers => numbers.sort((a, b) => a - b);
10 | exports.median = numbers => {
11 | const { length } = numbers;
12 | const middle = Math.floor(length / 2);
13 | return length % 2
14 | ? numbers[middle]
15 | : (numbers[middle - 1] + numbers[middle]) / 2;
16 | };
17 |
18 | exports.mode = numbers => {
19 | const counts = numbers.reduce(
20 | (acc, current) => acc.set(current, acc.get(current) + 1 || 1),
21 | new Map()
22 | );
23 |
24 | const maxCount = Math.max(...counts.values());
25 | const modes = [...counts.keys()].filter(
26 | number => counts.get(number) === maxCount
27 | );
28 |
29 | if (modes.length === numbers.length) {
30 | // 최빈값이 없음
31 | return null;
32 | }
33 |
34 | if (modes.length > 1) {
35 | // 최빈값이 여러개
36 | return modes;
37 | }
38 |
39 | // 최빈값이 하나
40 | return modes[0];
41 | };
42 |
--------------------------------------------------------------------------------
/javascript/stats.test.js:
--------------------------------------------------------------------------------
1 | const stats = require('./stats');
2 |
3 | describe('stats', () => {
4 | it('gets maximum value', () => {
5 | expect(stats.max([1, 2, 3, 4])).toBe(4);
6 | });
7 | it('gets minimum value', () => {
8 | expect(stats.min([1, 2, 3, 4])).toBe(1);
9 | });
10 | it('gets average value', () => {
11 | expect(stats.avg([1, 2, 3, 4, 5])).toBe(3);
12 | });
13 | describe('median', () => {
14 | it('sorts the array', () => {
15 | expect(stats.sort([5, 4, 1, 2, 3])).toEqual([1, 2, 3, 4, 5]);
16 | });
17 | it('gets the median for odd length', () => {
18 | expect(stats.median([1, 2, 3, 4, 5])).toBe(3);
19 | });
20 | it('gets the median for even length', () => {
21 | expect(stats.median([1, 2, 3, 4, 5, 6])).toBe(3.5);
22 | });
23 | });
24 | describe('mode', () => {
25 | it('has one mode', () => {
26 | expect(stats.mode([1, 2, 2, 2, 3])).toBe(2);
27 | });
28 | it('has no mode', () => {
29 | expect(stats.mode([1, 2, 3])).toBe(null);
30 | });
31 | it('has multiple mode', () => {
32 | expect(stats.mode([1, 2, 2, 3, 3, 4])).toEqual([2, 3]);
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/javascript/sum.js:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | function sumOf(numbers) {
6 | return numbers.reduce((acc, current) => acc + current, 0);
7 | }
8 |
9 | // 각각 내보내기
10 | exports.sum = sum;
11 | exports.sumOf = sumOf;
12 |
--------------------------------------------------------------------------------
/javascript/sum.test.js:
--------------------------------------------------------------------------------
1 | const { sum, sumOf } = require('./sum');
2 |
3 | describe('sum', () => {
4 | it('calculates 1 + 2', () => {
5 | expect(sum(1, 2)).toBe(3);
6 | });
7 |
8 | it('calculates all numbers', () => {
9 | const array = [1, 2, 3, 4, 5];
10 | expect(sumOf(array)).toBe(15);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/react-enzyme-test/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/react-enzyme-test/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/react-enzyme-test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-test",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@types/jest": "^24.0.13",
7 | "enzyme": "^3.9.0",
8 | "enzyme-adapter-react-16": "^1.13.1",
9 | "enzyme-to-json": "^3.3.5",
10 | "react": "^16.8.6",
11 | "react-dom": "^16.8.6",
12 | "react-scripts": "3.0.1"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | },
35 | "jest": {
36 | "snapshotSerializers": [
37 | "enzyme-to-json/serializer"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/react-enzyme-test/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/velopert/learn-react-testing/6c006c222684ac0048c3dcb14fc4bf3bdaf77c3b/react-enzyme-test/public/favicon.ico
--------------------------------------------------------------------------------
/react-enzyme-test/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/react-enzyme-test/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/react-enzyme-test/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | pointer-events: none;
9 | }
10 |
11 | .App-header {
12 | background-color: #282c34;
13 | min-height: 100vh;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | justify-content: center;
18 | font-size: calc(10px + 2vmin);
19 | color: white;
20 | }
21 |
22 | .App-link {
23 | color: #61dafb;
24 | }
25 |
26 | @keyframes App-logo-spin {
27 | from {
28 | transform: rotate(0deg);
29 | }
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/react-enzyme-test/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import HookCounter from './HookCounter';
4 |
5 | function App() {
6 | return (
7 |
17 | `;
18 |
--------------------------------------------------------------------------------
/rtl-tutorial/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/rtl-tutorial/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/rtl-tutorial/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/rtl-tutorial/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/rtl-tutorial/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import 'react-testing-library/cleanup-after-each';
2 | import 'jest-dom/extend-expect';
3 |
4 | // this is just a little hack to silence a warning that we'll get until react
5 | // fixes this: https://github.com/facebook/react/pull/14853
6 | const originalError = console.error;
7 | beforeAll(() => {
8 | console.error = (...args) => {
9 | if (/Warning.*not wrapped in act/.test(args[0])) {
10 | return;
11 | }
12 | originalError.call(console, ...args);
13 | };
14 | });
15 |
16 | afterAll(() => {
17 | console.error = originalError;
18 | });
19 |
--------------------------------------------------------------------------------