-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathchapter10.tex
1753 lines (1196 loc) · 111 KB
/
chapter10.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\chapter{Тестирование и Отладка}
За счет разделения React приложений на компоненты, их очень удобно тестировать. Есть множество инструментов для создания тестов с React, и в этой главе мы разберем самые популярные из них, чтобы понять, какую выгоду мы можем из них извлечь.
\textbf{Jest} -- тестовый фреймворк, который поддерживается силами \textit{Кристофером Пожером(Christopher Pojer)} из Facebook и членами сообщества; но ничто не мешает вам решить использовать \textbf{Mocha}. Мы посмотрим на оба способа создания лучшего тестового окружения.
Также вы узнаете о разнице между \textbf{Поверхностной отрисовкой (Shallow rendering)} и полной отрисовкой DOM с помощью \textbf{TestUtils} и \textbf{Enzyme}, как работает \textbf{Snapshot} тестирование и как собирать информацию о покрытии кода тестами.
После разбора самих инструментов мы перейдем к примеру покрытия тестами компонента из репозитория Redux и посмотрим на распространенные подходы, которые могут быть применены в сложных тестовых сценариях.
После разбора этой главы, вы сможете создать с нуля свое собственное тестовое окружение и написать тесты для компонентов вашего приложения.
В этой главе мы разберем следующие вопросы:
\begin{itemize}
\item Почему важно покрывать приложение тесты, и как это ускоряет разработку
\item Как настроить окружение с помощью Jest и начать писать тесты с TestUtils
\item Как создать такое же тестовое окружение с Mocha
\item Что такое Enzyme, и почему он рекомендуется для создания тестов React компонентов
\item Как создать тесты для компонента из реального приложения
\item Снимки (snapshots) Jest и процент покрытия с помощью библиотеки Istanbul
\item Основные способы тестирования компонентов высшего порядка и сложных страниц с множеством дочерних компонентов
\item Инструменты разработчика в React и подходы к обработке ошибок
\end{itemize}
\section{Польза от тестирования}
Тестирование web UI никогда не было простой задачей. Какие бы тесты мы не рассматривали, от модульных (unit) до сквозных (end-to-end), интерфейс всегда зависит от браузеров, взаимодействия с пользователем и множества других параметров, которые затрудняют создание оптимальной стратегии тестирования.
Если вы когда-либо писали сквозные тесты, то должны знать, как трудно получить стабильно воспроизводимые результаты из-за различных факторов влияющих на выполнение тестов (например, нестабильность сети). Помимо этого, пользовательские интерфейсы часто обновляются, чтобы улучшить конверсию или просто добавить новые функции.
Если тесты становится сложно писать и поддерживать, у разработчиков пропадает мотивация в их создании. С другой стороны тесты очень важны, так как увеличивают доверие к коду, что увеличивает скорость и качество разработки. Если какая-то часть кода покрыта хорошими тестами, то разработчик, даже если вносит изменения, может быть уверен, что этот код работает корректно и готов к поставке.
Часто разработчики могут быть сосредоточены над созданием новых возможностей для приложения, и в этот момент им может быть трудно понять, не ломают ли они уже существующий код. Наличие тестов может уберечь от регрессии приложения, так как их падение очень наглядно говорит о поломке в коде. Таким образом тесты добавляют уверенности в работоспособности кода и уменьшают время, необходимое для его релиза.
Также тесты помогают улучшить качество кода в целом. Даже если обнаруживается какая-либо ошибка в приложении, ничто не мешает, не только исправить эту ошибку, но и создать специальный тест, который воспроизводит эту ошибку. Такой прием позволит в будущем быстрее обнаруживать появление ранее встречаемых ошибок.
К счастью, React упрощает написание тестов для UI. Тестирование отдельных компонентов (или деревьев компонентов) не так трудоемко, по сравнению с полным сквозным тестирование, особенно если компоненты обладают своей строго ограниченной областью ответственности.
А если компоненты не обладают внутренним состоянием, то они могут тестироваться как обычные функции.
Еще одна великолепная возможность, которую принесли современные инструменты, это возможность запускать тесты при помощи Node и консоли. Потребность в запуске браузера для проверки тестов значительно замедляет процесс разработки и ухудшает воспроизводимость тестов; что собственно и исправляется запуском тестов в консоли.
Когда компоненты тестируются только в консоли, могут всплыть неожиданные вещи при из запуске в браузере, но в общем и целом это происходит крайне редко.
Когда мы тестируем React компоненты, мы хотим быть уверены, что они работают корректно для различных комбинаций параметров, которые им можно передать.
Также мы можем захотеть покрыть тестами различные состояния компонента, если таковые есть. Если состояние компонента меняется по нажатию на кнопку или какому-либо другому событию, то мы можем покрыть тестами обработчики событий, чтобы всегда быть уверенными, что они работают так, как должны работать.
После покрытия тестами основного функционала компонента, мы можем покрыть проверить \textbf{Пограничные случае (Edge cases)}. К пограничным случаям мы можем отнести ситуации, когда все параметры компонента приняли значение \textit{null}, или когда произошла какая-то ошибка. После того, как все тесты написаны, мы можем быть уверены в достаточной степени, что компонент ведет себя в соответствии с нашими ожиданиями.
Очень важно тестировать компоненты по отдельности, но это не гарантирует, что их совокупность также будет работать корректно. Как мы увидим далее, с React мы можем отрисовывать дерево элементов и тестировать взаимодействие компонентов внутри этого дерева.
Есть различные подходы в написании тестов, но один из самых популярных -- \textbf{Разработка через тестирование (Test Driven Development, TDD)}. Использование TDD подразумевает написание тестов перед написанием основного кода, за счет чего мы получаем возможность оценки корректности будущего кода до начала его разраотки.
Следование этому подходу помогает писать код лучше, так как мы задумываемся о его дизайне еще до того, как начнем писать сам код, что обычно ведет к повышению качества.
\section{Тестирование JavaScript с Jest}
Лучший способ научиться тестировать React компоненты... это протестировать React компоненты. Поэтому в этой части мы попробуем написать небольшие компоненты и покрыть их тестами.
Документация React говорит о том, что в Facebook для тестирования используется Jest. Но в общем случае ничто не запрещает вам использовать любой другой тестовый фреймворк.
А в следующей части вы научитесь тестировать компоненты при помощи Mocha.
Чтобы посмотреть, как работает Jest, мы создадим с нуля проект, установим все необходимые зависимости, и создадим компонент, который покроем тестами. Это будет весело!
Начнем с того, что создадим проект в пустой директории:
\begin{lstlisting}
npm init
\end{lstlisting}
После того, как \textit{package.json} будет создан, мы можем начать устанавливать зависимости. Первой из них будет сам Jest:
\begin{lstlisting}
npm install --save-dev jest
\end{lstlisting}
Для того, чтобы сказать npm, что мы хотим использовать команду \textit{jest} для запуска тестов, мы должны добавить соответствующую команду в файл \textit{package.json}:
\begin{lstlisting}
"scripts": {
"test": "jest"
},
\end{lstlisting}
Для того, чтобы иметь возможность использовать все возможности ES2015 и JSX, мы должны установить Babel с соответствующими плагинами:
\begin{lstlisting}
npm install --save-dev babel-jest babel-preset-es2015 babel-preset-react
\end{lstlisting}
Как вы уже можете догадаться, для конфигурации Babel нам понадобится файл \textit{.babelrc}, в котором мы укажем, какие пресеты мы хотим использовать в нашем проекте:
\begin{lstlisting}
{
"presets": ["es2015", "react"]
}
\end{lstlisting}
Само собой нам потребуются React и ReactDom, чтобы иметь возможность создавать и запускать React компоненты:
\begin{lstlisting}
npm install --save react react-dom
\end{lstlisting}
Настройка проекта закончена, мы можем запускать Jest для тестирования ES2015 и JSX кода, а также создавать и отрисовывать компоненты, но есть еще одна вещь, которую необходимо сделать.
Как мы уже сказали, мы хотим запускать тесты в консоли с Node. Но в этом случае мы не можем использовать ReactDOM, так как он требует DOM браузера.
Команда Facebook создала специальный инструмент, который называется \textit{TestUtils}. Этот инструмент позволяет без проблема тестировать React компоненты в любом тестовом фреймворке.
Давайте для начала его установим и посмотрим, какие возможности он предоставляет:
\begin{lstlisting}
npm i --save-dev react-addons-test-utils
\end{lstlisting}
Теперь у нас есть все необходимое для тестирования компонентов. TestUtils позволяет выполнять поверхностную (shallow) отрисовку компонентов или отрисовывать компоненты в специальный DOM, отделенный от браузера. Также эта библиотека позволяет получать ссылки на компоненты, отрисованные в DOM, для проверки их состояния в целях тестирования.
Также с TestUtils возможно симулировать события браузера для проверки работоспособности обработчиков событий.
Давайте начнем с создания компонента, который в дальнейшем будет покрывать тестами.
Мы создадим компонент \textit{Button}, который будет получать из параметров текст и отрисовывать кнопку с этим текстом. Также в нем будет обработчик событий для этой кнопки. Для начала мы создадим только скелет этого компонента, а затем создадим к нему тесты, чтобы следовать подходу TDD.
Нам будет необходимо создать компонент класс, так как TestUtils на данный момент не умеет работать с функциональными компонентами.
Создадим файл \textit{button.js} и импортируем в нем React:
\begin{lstlisting}
import React from 'react'
\end{lstlisting}
Теперь мы можем определить сам компонент:
\begin{lstlisting}
class Button extends React.Component
\end{lstlisting}
В компоненте на данный момент будет только метод render, который будет возвращать пустой \textit{div}:
\begin{lstlisting}
render() {
return <div />
}
\end{lstlisting}
И в конце добавим экспорт этого компонента:
\begin{lstlisting}
export default Button
\end{lstlisting}
Компонент подготовлен к покрытию тестами, теперь мы можем создать файл \textit{button.spec.js} и приступить к написанию тестов.
Jest ищет тесты во всех файлах, которые оканчиваются на \textit{.spec} и \textit{.test}, а также во всех файлах директории \textit{\_\_tests\_\_}; но вы можете изменить это поведение в настройках Jest, если этого требует ваш проект.
В начале файла \textit{button.spec.js} мы импортируем все необходимые зависимости:
\begin{lstlisting}
import React from 'react'
import TestUtils from 'react-addons-test-utils'
import Button from './button'
\end{lstlisting}
Нам нужен React, чтобы писать JSX код, TestUtils, на который мы посмотрим немного дальше, и только что созданный компонент\textit{Button}.
Для начала создадим простейший тест, чтобы убедиться, что система тестирования в принципе функционирует:
\begin{lstlisting}
test('works', () => {
expect(true).toBe(true)
})
\end{lstlisting}
Функция \textit{test} принимает два параметра: описание теста и функцию с реализацией самого теста. Внутри мы используем функцию \textit{expect} для передачи Jest объекта, относительно которого мы хотим выполнить предсказание. Функция \textit{expect} возвращает объект с методами, которые позволяют конкретизировать предсказание. Например, функция \textit{toBe} проверяет, что переданный объект в точности соответствует заданному.
Теперь мы можем выполнить в терминале команду:
\begin{lstlisting}
npm test
\end{lstlisting}
Вы должны увидеть следующий результат:
\begin{lstlisting}
PASS ./button.spec.js
works (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.48s
Ran all test suites.
\end{lstlisting}
Если вы увидели в выводе в консоль слово PASS, вы готовы для создания реальных тестов.
Как мы уже сказали, с помощью тестов мы хотим убедиться, что компонент корректно обрабатывает полученные параметры, а обработчики событий выполняют свою работу.
Существует два основных способа тестировать React компонента:
\begin{itemize}
\item Поверхностная отрисовка
\item Монтирование компонентов в специальный DOM
\end{itemize}
Начнем с первого из них, так как он проще для понимания. При поверхностной отрисовке, как можно догадаться из названия, отрисовывается не все дерево компонентов, а только его верхняя часть \textit{высотой 1}, относительно которой мы можем выполнять различные проверки.
Отрисовка одного уровня дерева означает, что мы будет тестировать исходный компонент независимо от дочерних сколь сложны они бы не были. Таким образом, отрисовка дочерних компонентов не будет проводиться в принципе, поэтому они никак не смогут повлиять на результаты теста.
Первый тест, который мы можем сделать, это проверить, что переданный компоненту текст отрисовывается внутри кнопки.
Для начала создадим сам тест:
\begin{lstlisting}
test('renders with text', () => {
\end{lstlisting}
Создадим переменную с заданным текстом, которую мы будем передавать в параметры проверяемого компонента:
\begin{lstlisting}
const text = 'text'
\end{lstlisting}
И теперь можно выполнить поверхностную отрисовку компонента, для чего достаточно следующих трех строк:
\begin{lstlisting}
const renderer = TestUtils.createRenderer()
renderer.render(<Button text={text} />)
const button = renderer.getRenderOutput()
\end{lstlisting}
Сначала мы создаем \textit{renderer}, с помощью которого отрисовываем компонент \textit{Button}, и в последней строчке получаем результат отрисовки.
Результат отрисовки будет выглядеть примерно следующим образом:
\begin{lstlisting}
{
'$$typeof': Symbol(react.element),
type: 'button',
key: null,
ref: null,
props: { onClick: undefined, children: 'text' },
_owner: null,
_store: {}
}
\end{lstlisting}
Если долго и пристально вглядываться, то можно увидеть в этом объекте React элемента. Его параметр \textit{props} отвечает за переданные элементу параметры, в том числе за дочерние (атрибут \textit{children}) элементы.
Теперь мы знаем, как выглядит результат отрисовки, а значит может легко проверить, что отрисовалась именно кнопка, а дочерним элементом является значение переменной \textit{text}:
\begin{lstlisting}
expect(button.type).toBe('button')
expect(button.props.children).toBe(text)
\end{lstlisting}
И не забудем в конце закрыть все скобки:
\begin{lstlisting}
})
\end{lstlisting}
Теперь, если вы запустите в консоли команду:
\begin{lstlisting}
npm test
\end{lstlisting}
Вы должны увидеть что-то следующего вида:
\begin{lstlisting}
FAIL ./button.spec.js
renders with text
expect(received).toBe(expected)
Expected value to be (using ===):
"button"
Received:
"div"
\end{lstlisting}
Тест упал, чего мы вообще говоря и ожидали, так как запустили тест для еще не реализованного компонента в соответствии с TDD подходом. Теперь мы можем вернуться к компоненту и поправить метод \textit{render} так, чтобы компонент проходил данный тест:
\begin{lstlisting}
render() {
return (
<button>
{this.props.text}
</button>
)
}
\end{lstlisting}
Теперь при запуске тестов нас должна встретить зеленая галочка:
\begin{lstlisting}
PASS ./button.spec.js
renders with text (9ms)
Test Suites: 1 passed, 1 total
Tests:
Snapshots:
Time:
Ran all test suites.
\end{lstlisting}
Поздравляю! Ваш первый тест для компонента, написанный в соответствии с TDD, выполнился успешно.
Теперь давайте посмотрим, как проверить, что компонент получил обработчик событий \textit{onClick}, и что этот обработчик вызывается при нажатии на кнопку.
Но перед тем, как мы начнем, нужно рассказать про две концепции: моки (mock) и открепленный (detached) DOM.
Первая упрощает проверку работы функций внутри теста. В данном тесте мы хотим передать компоненту функцию через параметр \textit{onClick} и проверить, что функция вызывается, когда происходит нажатие на кнопку.
Для того, чтобы сделать это, нам нужно создать специальную \textbf{мок (mock)} функцию (в других фреймворках такая функция может иметь другое название, например \textit{spy}). Такая функция работает как обыкновенная, но расширена дополнительными возможностями.Например, можно проверить, сколько раз и с какими параметрами была вызвана функция.
Для того, чтобы создать мок функцию при помощи Jest, мы можем использовать \textit{jest.fn()}.
Вторую концепцию нам нужно разобрать из-за того, что мы не можем симулировать события DOM с помощью \textit{TestUtils} при поверхностной отрисовке.
Это происходит из-за того, что для тестирования событий с \textit{TestUtils}, нам нужны реальные компоненты, а не React элементы.
Поэтому, для тестирования событий браузера, нам необходимо отрисовать наш компонент в открепленный DOM. Отрисовка компонента в полноценный DOM требует наличия браузера, но вместе с Jest идет специальный DOM, в который можно что-то отрисовать из консоли.
Отрисовка компонента в открепленный DOM несколько отличается от поверхностной отрисовки, поэтому давайте посмотрим, как это будет выглядеть в коде.
Для начала создадим новый тест:
\begin{lstlisting}
test('fires the onClick callback', () => {
\end{lstlisting}
Создадим мок функцию \textit{onClick} при помощи Jest:
\begin{lstlisting}
const onClick = jest.fn()
\end{lstlisting}
Теперь мы отрисуем компонент в DOM полностью:
\begin{lstlisting}
const tree = TestUtils.renderIntoDocument(
<Button onClick={onClick} />
)
\end{lstlisting}
Если мы распечатаем \textit{tree} в консоль, то увидим не React элемент, а полноценный компонент.
Из-за этого мы уже не можем просто проверить, что вернула функции \textit{renderIntoDocument}, но с помощью специального метода \textit{TestUtils} мы можем получить элемент кнопки, которая нас интересует:
\begin{lstlisting}
const button = TestUtils.findRenderedDOMComponentWithTag(
tree,
'button'
)
\end{lstlisting}
Как можно догадаться из названия функции, она ищет внутри дерева элемент с заданным тегом.
Теперь мы можем воспользоваться другим методом из \textit{TestUtils} для симуляции события:
\begin{lstlisting}
TestUtils.Simulate.click(button)
\end{lstlisting}
Объект \textit{Simulate} предоставляет функции, которые имеют названия, аналогичные названиям событий, и принимают один параметр для цели события.
И в конце выполняем проверку того, что функция была вызвана:
\begin{lstlisting}
expect(onClick).toBeCalled()
\end{lstlisting}
То есть мы просто проверяем, что \textit{мок} функция была вызвана.
Если мы снова запустим тесты, то увидим сообщение об ошибке, что ожидаемо, так как мы еще не реализовали работу функции \textit{onClick}:
\begin{lstlisting}
FAIL ./button.spec.js
fires the onClick callback
expect(jest.fn()).toBeCalled()
Expected mock function to have been called.
\end{lstlisting}
Именно так мы и работает при TDD подходе. Теперь вернемся в файл \textit{button.js} и реализуем обработчик событий:
\begin{lstlisting}
render() {
return (
<button onClick={this.props.onClick}>
{this.props.text}
</button>
)
}
\end{lstlisting}
Теперь тесты должны показывать зеленый свет:
\begin{lstlisting}
PASS ./button.spec.js
renders with text (10ms)
fires the onClick callback (17ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.401s, estimated 2s
Ran all test suites.
\end{lstlisting}
Теперь наш компонент полностью протестирован и реализован в соответствии с написанными тестами.
\section{Гибкий тестовый фреймворк Mocha}
В этой части мы проделаем то же самое, чтобы показать, что вы можете использовать с React любой тестовый фреймворк по вашему желанию. Также будет полезным увидеть разницу между интегрированным фреймворком Jest, который старается все автоматизировать для более плавного использования (прим.пер. smooth developer experience что бы это не значило), и Mocha, который не делает никаких предположений относительно того, какие инструменты вам нужны. С Mocha вы можете установить любые библиотеки, которые вам нужны для тестирования React компонентов.
Для начала создадим новый npm проект в пустой директории:
\begin{lstlisting}
npm init
\end{lstlisting}
И добавим саму библиотеку \textit{mocha}:
\begin{lstlisting}
npm install --save-dev mocha
\end{lstlisting}
Так же как и для Jest, чтобы писать ES2015 код и JSX, нам потребуется Babel с парой плагинов:
\begin{lstlisting}
npm install --save-dev babel-register babel-preset-es2015 babel-preset-react
\end{lstlisting}
Теперь, после установки Mocha и Babel, мы можем добавить скрипт для запуска тестов:
\begin{lstlisting}
"scripts": {
"test": "mocha --compilers js:babel-register"
},
\end{lstlisting}
Мы говорим \textit{npm}, что для выполнения команды \textit{test} необходимо запустить \textit{mocha} с флагом \textit{compilers} (для предварительного прогона исходного кода через \textit{Babel}).
Теперь добавим React и ReactDOM:
\begin{lstlisting}
npm install --save react react-dom
\end{lstlisting}
А также \textit{TestUtils}, который позволяет нам отрисовывать компоненты в тестовом окружении:
\begin{lstlisting}
npm install --save-dev react-addons-test-utils
\end{lstlisting}
Базовый инструментарий для работы с Mocha готов, но для того, чтобы привести все в соответствие с Jest, нам понадобится еще пара-тройка библиотек.
Первая из них -- \textit{chai}, которая позволяет писать проверки в том же стиле, что и в Jest. Вторая -- \textit{chai-spies}, с которой мы можем проводить шпионские операции для проверки функций, таких как \textit{onClick}.
И последняя библиотека \textit{jsdom} позволяет нам создавать открепленный DOM, чтобы TestUtils могли отрисовывать компоненты без реального браузера:
\begin{lstlisting}
npm install --save-dev chai chai-spies jsdom
\end{lstlisting}
Теперь мы готовы приступить к написанию тестов, для чего можем использовать созданный ранее файл \textit{button.js}. Мы уже реализовали компонент, поэтому мы не будем следовать TDD, но сейчас наша главная задача состоит в том, чтобы увидеть разницу между двумя тестовыми фреймворками.
Mocha ожидает, что тесты будут находиться в директории \textit{test}, поэтому мы можем создать ее и файл \textit{button.spec.js} внутри нее.
В начале файла импортируем все необходимые зависимости:
\begin{lstlisting}
import chai from 'chai'
import spies from 'chai-spies'
import { jsdom } from 'jsdom'
import React from 'react'
import TestUtils from 'react-addons-test-utils'
import Button from '../button'
\end{lstlisting}
Как вы можете заметить, после тестов с Jest, необходимо импортировать больше различных библиотек. Это связано с тем, что Mocha предоставляет вам самим выбрать, какие вспомогательные инструменты вам нужны.
Далее необходимо указать библиотеке \textit{chai} использовать \textit{spies}:
\begin{lstlisting}
chai.use(spies)
\end{lstlisting}
Сразу вытащим пару функция из \textit{chai}, которые потребуются нам далее в тестах:
\begin{lstlisting}
const { expect, spy } = chai
\end{lstlisting}
Далее мы создадим экземпляр \textit{jsdom} и установим его как DOM для отрисовки компонентов:
\begin{lstlisting}
global.document = jsdom('')
global.window = document.defaultView
\end{lstlisting}
И вот теперь мы можем создать первый тест. Обычно с Mocha (прим.пер. как и с Jest на самом деле) используется две функции для написания тестов: \textit{describe}, которая описывает набор тестов, и \textit{it}, внутри которой непосредственно описываются тесты.
В данном случае мы описываем поведение кнопки:
\begin{lstlisting}
describe('Button', () => {
\end{lstlisting}
И затем мы создаем первый тест, в котором проверяем, что у компонента правильные тип и текст:
\begin{lstlisting}
it('renders with text', () => {
\end{lstlisting}
Создадим переменную с текстом, которую будем использовать для проверки текста в кнопке:
\begin{lstlisting}
const text = 'text'
\end{lstlisting}
Далее сделаем поверхностную отрисовку компонента, как мы делали это до этого:
\begin{lstlisting}
const renderer = TestUtils.createRenderer()
renderer.render(<Button text={text} />)
const button = renderer.getRenderOutput()
\end{lstlisting}
И в конце выполним проверки:
\begin{lstlisting}
expect(button.type).to.equal('button')
expect(button.props.children).to.equal(text)
\end{lstlisting}
Как вы можете заметить, есть небольшие синтаксические различия. Вместо функциил \textit{toBe} появилась \textit{to.equal} из библиотеки \textit{chai}. Но результат одинаковый: сравнение двух значений.
Не забудем закрыть скобки для первого теста:
\begin{lstlisting}
})
\end{lstlisting}
В следующем тесте мы будем проверять, что вызывается функция обратного вызова \textit{onChange}:
\begin{lstlisting}
it('fires the onClick callback', () => {
\end{lstlisting}
Создадим мок функцию с помощью \textit{spy}, аналогично тому, как до этого создавали при помощи \textit{jest.fn}:
\begin{lstlisting}
const onClick = spy()
\end{lstlisting}
Отрисовываем компонент в открепленный DOM при помощи \textit{TestUtils}:
\begin{lstlisting}
const tree = TestUtils.renderIntoDocument(
<Button onClick={onClick} />
)
\end{lstlisting}
А с помощью \textit{tree} мы можем найти нужный элемент в дереве:
\begin{lstlisting}
const button = TestUtils.findRenderedDOMComponentWithTag(
tree,
'button'
)
\end{lstlisting}
Следующий шаг -- симуляция нажатия кнопки:
\begin{lstlisting}
TestUtils.Simulate.click(button)
\end{lstlisting}
И последним шагом можно проверить, была ли вызвана функция:
\begin{lstlisting}
expect(onClick).to.be.called()
\end{lstlisting}
И снова, хоть синтаксис немного и поменялся, но общая идея осталась той же, мы просто проверяем у функции, была ли она вызвана.
Теперь, если мы запустим \textit{npm test} в корневой директории, то увидим следующее сообщение:
\begin{lstlisting}
Button
renders with text
fires the onClick callback
2 passing (847ms)
\end{lstlisting}
Это означает, что наш тест выполнился успешно, а мы готовы использовать Mocha для тестирования наших компонентов.
\section{JavaScript инструменты для тестирования React}
На данный момент вы должны понимать, как тестировать компоненты при помощи Jest и Mocha, а также плюсы и минусы обоих подходов.
Также вы познакомились с TestUtils и узнали о двух способах отрисовки компонент: поверхностном и в открепленный DOM.
Однако вы могли заметить, что с TestUtils не всегда легко получить доступ к нужным элементам и их параметрам.
По этой причине разработчики из \textit{AirBnb} создали Enzyme, тестовый фреймворк, который работает поверх TestUtils и урощает работу с отрисованными компонентами.
API Enzyme приятно и схоже с jQuery, а также предоставляет удобные методы для работы с компонентами, их состоянием и параметрами.
Давайте посмотрим, как будут выглядеть уже написанные нами тесты, если мы перепишем их с Enzyme.
Давайте вернемся к проекту, где мы писали тесты с Jest, и добавим в него Enzyme:
\begin{lstlisting}
npm install --save-dev enzyme
\end{lstlisting}
Теперь откроем файл \textit{button.spec.js} и поправим в нем импорты следующим образом:
\begin{lstlisting}
import React from 'react'
import { shallow } from 'enzyme'
import Button from './button'
\end{lstlisting}
Как вы можете увидеть, вместо \textit{TestUtils} мы импортируем \textit{shallow} из Enzyme. Из названия можно понять, что она выполняет поверхностную отрисовку компонента, но также обладает дополнительными возможностями.
Прежде всего, Enzyme позволяет эмулировать события даже при поверхностной отрисовке компонентов, чего мы не могли сделать с \textit{TestUtils}. И помимо этого, \textit{shallow} из Enzyme возвращает не просто React элемент, а \textbf{обертку (ShallowWrapper)} над ним, специальный объект с дополнительными параметрами и методами, которые мы разберем чуть дальше.
Давайте начнем с теста, который называется \textbf{renders with text}. Первая строка, где мы определяем переменную с текстом, остается той же самой:
\begin{lstlisting}
const text = 'text'
\end{lstlisting}
Поверхностная отрисовка компонента становится интуитивно понятнее и выразительнее. Три строки кода с использованием \textit{TestUtils} мы можем заменить одной:
\begin{lstlisting}
const button = shallow(<Button text={text} />)
\end{lstlisting}
Объект \textit{button} представляет собой обертку над React элементом со вспомогательными методами, которые мы можем использовать для выполнения проверок:
\begin{lstlisting}
expect(button.type()).toBe('button')
expect(button.text()).toBe(text)
\end{lstlisting}
Теперь, вместо проверки параметров React элемента, названия которых могут измениться, мы используем библиотечные функции, которые абстрагируют внутри себя поиск нужных параметров (прим.пер. особенно если библиотека вовремя обновляется).
Функция \textit{type} проверяет тип элемента, а функиця \textit{text}, соответственно, текст внутри элемента. В нашем случае, это тот текст, который мы передали через параметры.
Теперь весь тест должен выглядеть следующим образом:
\begin{lstlisting}
test('renders with text', () => {
const text = 'text'
const button = shallow(<Button text={text} />)
expect(button.type()).toBe('button')
expect(button.text()).toBe(text)
})
\end{lstlisting}
Теперь тест выглядит лаконичнее и чище чем до этого.
Теперь поправим тест, который проверяет работу события \textit{onClick}. Снова, первая строчка остается той же:
\begin{lstlisting}
const onClick = jest.fn()
\end{lstlisting}
Мы можем также использовать мок функции из Jest для проверки срабатывания обработчиков событий.
Мы можем заменить строку, где мы использовали \textit{renderIntoDocument} для отрисовки компонента в открепленный DOM следующей:
\begin{lstlisting}
const button = shallow(<Button onClick={onClick} />)
\end{lstlisting}
Нам не нужно использовать \textit{findRenderedDOMComponentWithTag} для поиска кнопки, так как \textit{shallow} и так возвращает ссылку на нее.
Синтаксис для вызова эмуляции события немного отличается от \textit{TestUtils}, но все также интуитивно понятен:
\begin{lstlisting}
button.simulate('click')
\end{lstlisting}
У каждой обертки есть метод \textit{simulate}, который принимает имя события и допольнительные аргументы, которые в данном случае нам не нужны, но мы их разберем, когда будем разбираться с тестированием форм.
Проверка, была ли вызвана функция, остается той же:
\begin{lstlisting}
expect(onClick).toBeCalled()
\end{lstlisting}
Весь код теста выглядит следующим образом:
\begin{lstlisting}
test('fires the onClick callback', () => {
const onClick = jest.fn()
const button = shallow(<Button onClick={onClick} />)
button.simulate('click')
expect(onClick).toBeCalled()
})
\end{lstlisting}
Переход на Enzyme предельно прост, и при этом значительно улучшает читаемость кода.
Библиотека предоставляет множество полезных методом, таких как поиск вложенных элементов или поиск элементов по имени класса.
Есть методы для выполнения проверок параметров у компонентов, а также установка конкретных состояния или контекста.
Помимо поверхностной отрисовки, которой в случае Enzyme хватает для большинства случаем, библиотека предоставляет метод \textit{mount}, который отрисовывает компонент в DOM.
\section{Пример тестов из реального мира}
На данный момент мы разобрались, как настроить тестовое окружение и посмотрели на разные тестовые фреймворки. Пришло время посмотреть на тестирование компонентов из реального мира.
Компонент \textit{Button} из предыдущего примера был великолепен, и мы должны стремиться сохранять компоненты настолько простыми, насколько это возможно. Но порой нам приходится реализовывать какую-либо логику внутри компонентов, а также хранить состояние, что несколько усложняет задачу тестирования.
На этот раз мы собираемся протестировать компонент \textbf{TodoTextInput} из примера Redux \textbf{TodoMVC}:
\begin{lstlisting}
https://github.com/reactjs/redux/blob/master/examples/todomvc/src/components/TodoTextInput.js
\end{lstlisting}
Вы можете скопировать его в ваш \textit{Jest} проект.
Это отличный пример для написания тестов, так как у компонента есть несколько параметров, его имя класса (прим.пер. я буду называть именем класса \textit{className} для CSS, чтобы не путать с ключевым словом \textit{class} JavaScript) меняется в соответствии с полученными параметрами, а также у него есть три обработчика с небольшим количеством логики, которую также стоит протестировать.
TodoMVC -- пример создания \textit{стандартного} приложения при помощи различных фреймворков для их сравнения, что должно помочь разработчика в выборе между ними.
В результате у нас есть простенькое приложение, в котором можно добавлять задачи (to-do) и отмечать их выполнение. Для наших целей мы возьмем компонент, который отвечает за поле ввода для создания и редактирования задач.
Имеет смысл, сначала пробежаться по коду самого компонента, чтобы понимать, что именно мы собираемся тестировать.
Начинается компонент с определения соответствующего ему класса:
\begin{lstlisting}
class TodoTextInput extends Component
\end{lstlisting}
В данном случае \textit{propTypes} определены при помощи свойства класса:
\begin{lstlisting}
static propTypes = {
onSave: PropTypes.func.isRequired,
text: PropTypes.string,
placeholder: PropTypes.string,
editing: PropTypes.bool,
newTodo: PropTypes.bool
}
\end{lstlisting}
Для того, чтобы свойства класса поддерживались с Babel, необходимо добавить еще один плагин:
\begin{lstlisting}
npm install --save-dev babel-plugin-transform-class-properties
\end{lstlisting}
И затем добавить этот плагин к остальным плагина в \textit{.babelrc}:
\begin{lstlisting}
"plugins": ["transform-class-properties"]
\end{lstlisting}
Состояние компонента также определено через свойство класса:
\begin{lstlisting}
state = {
text: this.props.text || ''
}
\end{lstlisting}
Значение по умолчанию может быть пустой строкой или установлено из параметра \textit{text (this.props.test)}.
Далее идут три обработчика событий, каждый из которых представляет из себя стрелочную функцию (поэтому нет необходимости прикреплять их к экземплярам класса в конструкторе), которая также сохранена как свойство класса.
Первый из них -- обработчик окончания ввода (\textit{submit}):
\begin{lstlisting}
handleSubmit = e => {
const text = e.target.value.trim()
if (e.which === 13) {
this.props.onSave(text)
if (this.props.newTodo) {
this.setState({ text: '' })
}
}
}
\end{lstlisting}
Функция получает объект события, проверяет, что была нажата клавиша \textit{Enter (13)}, убирает пробелы из введенной строки и сохраняет при помощи функции \textit{this.props.onSave}. Если параметр \textit{newTodo} -- \textit{true}, то сбрасывает поле ввода для создания новой задачи.
Следующий обработчик будет отслеживать изменения в поле ввода:
\begin{lstlisting}
handleChange = e => {
this.setState({ text: e.target.value })
}
\end{lstlisting}
Помимо того, что этот обработчик также определен через свойство класса, можно отметить, что оно сохраняет значение контролируемого поля ввода внутри состояния компонента.
И последний обработчик для отслеживания момента, когда пользователь убирает фокус с поля ввода (\textit{blur}):
\begin{lstlisting}
handleBlur = e => {
if (!this.props.newTodo) {
this.props.onSave(e.target.value)
}
}
\end{lstlisting}
Он вызывает функцию \textit{onSave}, если значение параметра \textit{newTodo} равно \textit{false}.
И в конце находится метод \textit{render}, в котором определен элемент \textit{input} со всеми этими параметрами:
\begin{lstlisting}
render() {
return (
<input className={
classnames({
edit: this.props.editing,
'new-todo': this.props.newTodo
})}
type="text"
placeholder={this.props.placeholder}
autoFocus="true"
value={this.state.text}
onBlur={this.handleBlur}
onChange={this.handleChange}
onKeyDown={this.handleSubmit} />
)
}
\end{lstlisting}
Для применения нужного имени класса используется функция \textit{classnames}, созданная \textit{Джедом Уотсоном (Jed Watson)}. Она удобна для расчета имени класса, которое зависит от различных логических выражений.
Также установлены несколько статических атрибутов (\textit{type} и \textit{autofocus}), через атрибут \textit{text} передается текст для управления значением поля ввода, и через соответствующие атрибуты переданы обработчики событий.
Перед тем, как начать, стоит понять, что именно мы собираемся тестировать и почему. Глядя на этот компонент, несложно выделить наиболее важные для покрытия тестами части. В данном случае, вы можете думать о данном компоненте как о коде, пришедшем в наследство от других команд (legacy code), или как о коде, который вы можете найти в новой компании.
Следующий список отражает функционал компонента, который в большей или меньшей степени подходит для покрытия тестами:
\begin{itemize}
\item Состояние компонента проинициализировано значением, пришедшим в параметрах
\item Параметр \textit{placeholder} корректно передается в поле ввода
\item Применяется правильное имя класса
\item Состояние компонента изменяется при вводе данных пользователем
\item Функция \textit{onSave} вызывается корректно для различных состояний и условий
\end{itemize}
Пришло время начать писать код. Мы начнем с того, что создадим файл \textit{TodoTextInput.spec.js} со следующими импортами:
\begin{lstlisting}
import React from 'react'
import { shallow } from 'enzyme'
import TodoTextInput from './TodoTextInput'
\end{lstlisting}
Мы импортируем сам React, функцию \textit{shallow} из Enzyme и компонент, который будет тестировать. Также создадим функцию, которую будет передавать в параметр \textit{onSave} в некоторых тестах:
\begin{lstlisting}
const noop = () => {}
\end{lstlisting}
Теперь мы можем создать первый тест, в котором проверим, что значение по умолчанию устанавливается из полученных параметров:
\begin{lstlisting}
test('sets the text prop as value', () => {
const text = 'text'
const wrapper = shallow(
<TodoTextInput text={text} onSave={noop} />
)
expect(wrapper.prop('value')).toBe(text)
})
\end{lstlisting}
Здесь все просто: создаем текстовую переменную, а затем выполняем поверхностную отрисовку компонента с передачей в него этой переменной. Также мы передаем функцию \textit{noop} в параметр \textit{onSave}, так как этот параметр является необходимым для компонента.
Далее мы выполняем проверку того, что значение в полученном элементе идентично значению переменной. Теперь, если мы запустим тест, то должны получить следующий результат:
\begin{lstlisting}
PASS ./TodoTextInput.spec.js
sets the text prop as value (10ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.384s
Ran all test suites.
\end{lstlisting}
Великолепно, продолжим писать тесты. Следующий тест будет очень похож на предыдущий за тем исключением, что мы будем проверять значение свойства \textit{placeholder}:
\begin{lstlisting}
test('uses the placeholder prop', () => {
const placeholder = 'placeholder'
const wrapper = shallow(
<TodoTextInput placeholder={placeholder} onSave={noop} />
)
expect(wrapper.prop('placeholder')).toBe(placeholder)
})
\end{lstlisting}
Можно запустить тесты, оба должны светить зеленым светом.
Попробуем написать что-нибудь поинтереснее. Например, проверим, что имя класса меняется в соответствии полученным параметрам:
\begin{lstlisting}
test('applies the right class names', () => {
const wrapper = shallow(
<TodoTextInput editing newTodo onSave={noop} />
)
expect(wrapper.hasClass('edit new-todo')).toBe(true)
})
\end{lstlisting}
В этом тесте мы добавили компоненту два параметра (\textit{editing} и \textit{newTodo}), а затем проверили, что соответствующие классы добавились в имя класса.
Было бы лучше проверить каждый из классов отдельно, но идея должна быть понятна.
Следующий тест будет несколько сложнее, потому что теперь мы хотим проверить реакцию компонента на событие нажатия клавиши (key down).
Проверим, что при нажатии клавиши \textit{Enter}, вызывается функция \textit{onSave} с текущим значением поля ввода:
\begin{lstlisting}
test('fires onSave on enter', () => {
const onSave = jest.fn()
const value = 'value'
const wrapper = shallow(<TodoTextInput onSave={onSave} />)
wrapper.simulate('keydown', { target: { value }, which: 13 })
expect(onSave).toHaveBeenCalledWith(value)
})
\end{lstlisting}
Сначала мы создаем мок функцию при помощи \textit{jest.fn()}, далее создаем переменную для установки значения поля ввода и отрисовываем компонент. После этого мы симулируем событие \textit{keydown} с кодом клавиши \textit{Enter (13)}.
У объекта события есть два параметра: \textit{target} со ссылкой на элемент, инициировавший событие, и \textit{which} с кодом нажатой клавиши.
И в конце выполняем проверку, что функция \textit{onSave} была вызвана со значением поля ввода.
Теперь \textit{npm test} должен сказать, что 4 теста прошли успешно.
С помощью теста, похожего на предыдущий, мы можем проверить, что при нажатии отличной от \textit{Enter} клавиши функция \textit{onSave} не вызывается:
\begin{lstlisting}
test('does not fire onSave on key down', () => {
const onSave = jest.fn()
const wrapper = shallow(<TodoTextInput onSave={onSave} />)
wrapper.simulate('keydown', { target: { value: '' } })
expect(onSave).not.toBeCalled()
})
\end{lstlisting}
Тест очень похож на предыдущий за исключением выполняемый проверки, в которой на этот раз используется параметр \textit{.not}. Как можно догадаться, таким образом мы говорим, что ожидаем \textit{false} из вызова функции \textit{toBeCalled}.
Как вы могли заметить, синтаксис вызова проверок очень похож на разговорный язык.
У нас уже есть 5 зеленых тестов, двигаемся к следующему:
\begin{lstlisting}
test('clears the value after save if new', () => {
const value = 'value'
const wrapper = shallow(<TodoTextInput newTodo onSave={noop} />)
wrapper.simulate('keydown', { target: { value }, which: 13 })
expect(wrapper.prop('value')).toBe('')
})
\end{lstlisting}
Отличие на этот раз в том, что мы передали параметр \textit{newTodo}, который заставляет сбрасываться значение поля ввода по нажатию клавиши \textit{Enter}.
Следущий тест:
\begin{lstlisting}
test('updates the text on change', () => {
const value = 'value'
const wrapper = shallow(<TodoTextInput onSave={noop} />)
wrapper.simulate('change', { target: { value } })
expect(wrapper.prop('value')).toBe(value)
})
\end{lstlisting}
Этот тест проверяет, что контролируемое поле ввода работает корректно. Если в вашем приложение есть формы, то вам обязательно нужны такого рода тесты.