Статьи

Пример модульного тестирования ReactJS

В этой статье мы рассмотрим модульное тестирование кода ReactJS. Написание модульных тестов для кода в наши дни является нормой, что делает тестирование кода менее запоздалым. Мы рассмотрим некоторые фреймворки модульного тестирования для ReactJS и их настройку. Вокруг ReactJS существует здоровая экосистема инструментов тестирования и каркасов, и мы можем выбирать в соответствии с нашими предпочтениями. Однако в этой статье я опишу использование Jest Testing Framework вместе с библиотекой Enzyme Helper.

Enzyme helper library – это абстракция, построенная на основе более подробного и низкоуровневого API React Test Utils.

В этом примере мы создаем примитивное калькуляторное приложение и пишем тесты для проверки нашего кода. Для этого примера мы используем следующий набор инструментов и фреймворков. Вы можете переключиться на свои предпочтения для некоторых из дополнительных инструментов, перечисленных ниже:

1. Начальная структура приложения

Мы создаем нашу начальную структуру приложения с помощью инструмента create-реагировать на приложение. Для начала выполните приведенную ниже команду для создания каркаса нашего приложения ReactJS.

1
>npx create-react-app unit-testing .

Эта команда создает наше модульное тестирование приложения, и его структура должна напоминать скриншот ниже

Модульное тестирование ReactJS - структура проекта
Структура проекта

2. Написание нашего первого теста

Преимущество create-реагировать на приложение состоит в том, что наше приложение поставляется с предварительно настроенной средой тестирования Jest. Да, нам не нужно выполнять никаких дополнительных настроек. Мы можем начать писать наши тесты с самого начала. Итак, давайте начнем писать наш первый тест. Но прежде чем мы это сделаем, нам нужно проверить некоторые функциональные возможности. Для этого теста я написал кодовое приложение для калькулятора. Он имеет два нижеследующих компонента, нам пока не нужно вдаваться в подробности их реализации. Вместо этого сосредоточьтесь на тестировании этих компонентов.

Calculator.js

01
02
03
04
05
06
07
08
09
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
import React, { useState } from 'react';
import Keypad from "./Keypad";
 
function Calculator() {
    const [result, setResult] = useState(0);
    const [operator, setOperator] = useState("");
    const [operand2, setOperand2] = useState(0);
    const [operand1, setOperand1] = useState(0);
    const handleChange = ({ target }) => {
        setResult(target.value);
    }
    const handleEqualsClick = ({ target }) => {
        if (operand1 && operand2 && operator) {
            switch (operator) {
                case "/": setResult(operand1 / operand2);
                    setOperand1(operand1 / operand2);
                    break;
                case "X": setResult(operand1 * operand2);
                    setOperand1(operand1 * operand2);
                    break;
                case "+": setResult(operand1 + operand2);
                    setOperand1(operand1 + operand2);
                    break;
                case "-": setResult(operand1 - operand2);
                    setOperand1(operand1 - operand2);
                    break;
                default:
                    break;
            }
            setOperand2(0);
            setOperator("");
        }
    }
    const handleClearClick = ({ target }) => {
        setResult(0);
        setOperator("");
        setOperand2(0);
        setOperand1(0);
    }
    const handleKeypadClick = ({ target }) => {
        if (!operator) {
            setOperand1(+(operand1 + "" + target.value));
            setResult(+(operand1 + "" + target.value));
        } else {
            setOperand2(+(operand2 + "" + target.value));
            setResult(+(operand2 + "" + target.value));
        }
    }
    const handleOperatorClick = ({ target }) => {
        if (operator) {
            handleEqualsClick({ target });
        }
        setOperator(target.value);
        setOperand2(0);
    }
    return <div className="container-fluid">
        <div className="row">
            <input type="text" readOnly onChange={handleChange} value={result} />
        </div>
        <div className="row">
            <Keypad onEqualsClick={handleEqualsClick}
                onClearClick={handleClearClick}
                onKeyClick={handleKeypadClick}
                onOperatorClick={handleOperatorClick} />
        </div>
    </div>;
}
export default Calculator;

Keypad.js

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from 'react';
 
function Keypad(props) {
 
    return <div className="container">
        <div className="row">
            <button className="btn btn-info m-1" value="1" onClick={props.onKeyClick}>1</button>
            <button className="btn btn-info m-1" value="2" onClick={props.onKeyClick}>2</button>
            <button className="btn btn-info m-1" value="3" onClick={props.onKeyClick}>3</button>
            <button className="btn btn-info m-1" value="/" onClick={props.onOperatorClick}>/</button>
        </div>
        <div className="row">
            <button className="btn btn-info m-1" value="4" onClick={props.onKeyClick}>4</button>
            <button className="btn btn-info m-1" value="5" onClick={props.onKeyClick}>5</button>
            <button className="btn btn-info m-1" value="6" onClick={props.onKeyClick}>6</button>
            <button className="btn btn-info m-1" value="X" onClick={props.onOperatorClick}>X</button>
        </div>
        <div className="row">
            <button className="btn btn-info m-1" value="7" onClick={props.onKeyClick}>7</button>
            <button className="btn btn-info m-1" value="8" onClick={props.onKeyClick}>8</button>
            <button className="btn btn-info m-1" value="9" onClick={props.onKeyClick}>9</button>
            <button className="btn btn-info m-1" value="-" onClick={props.onOperatorClick}>-</button>
        </div>
        <div className="row">
            <button className="btn btn-info m-1" value="Clear" onClick={props.onClearClick}>C</button>
            <button className="btn btn-info m-1" value="0" onClick={props.onKeyClick}>0</button>
            <button className="btn btn-info m-1" value="=" onClick={props.onEqualsClick}>=</button>
            <button className="btn btn-info m-1" value="+" onClick={props.onOperatorClick}>+</button>
        </div>
    </div>;
}
export default Keypad;

Мы пишем тесты в файлах с тем же именем, что и компонент, который мы тестируем, но заканчиваем в test.js. Затем они выбираются Jest, когда мы тестируем, и тесты, написанные внутри, выполняются. Давайте создадим два файла для проверки каждого из наших двух компонентов выше. Наш первый тест – проверить, работают ли вышеуказанные компоненты без сбоев. Тест для этого выглядит следующим образом:

Keypad.test.js

1
2
3
4
5
6
7
8
9
import React from 'react';
import ReactDOM from 'react-dom';
import Keypad from './Keypad';
 
it('renders without crashing', () => {
    const div = document.createElement('div');
    ReactDOM.render(<Keypad />, div);
    ReactDOM.unmountComponentAtNode(div);
});

Calculator.test.js

1
2
3
4
5
6
7
8
9
import React from 'react';
import ReactDOM from 'react-dom';
import Calculator from './Calculator';
 
it('renders without crashing', () => {
    const div = document.createElement('div');
    ReactDOM.render(<Calculator />, div);
    ReactDOM.unmountComponentAtNode(div);
});

Каждый тест начинается с вызова его функции с первым параметром, описывающим ожидания, как в «он рендерится без сбоев…». Это соглашение сопровождается каждым тестом, который мы пишем для всех компонентов. Второй параметр – это обратный вызов с нашим тестом, формирующий тело обратного вызова. В приведенных выше тестах мы визуализируем наши компоненты в DOM с использованием ReactDOM.

Теперь для запуска этих тестов мы запускаем следующую команду из терминала в корне проекта.

1
>npm test

Это запускает тест, и результаты отображаются, как показано ниже:

Модульное тестирование ReactJS - Результаты проекта
Выход проекта

Мы рассмотрим более сложные сценарии в следующих разделах

3. Тестирование снимков

Концепция тестирования моментальных снимков работает просто путем сравнения эталонного снимка пользовательского интерфейса с текущим пользовательским интерфейсом, сгенерированным тестом. В случае возникновения аномалии это может указывать на проблему, приводящую к непреднамеренному изменению пользовательского интерфейса компонента. Но это также может быть связано с преднамеренным и преднамеренным изменением макета. В последнем нам нужно обновить сохраненные снимки. Ниже мы рассмотрим оба случая, когда тестируем наш компонент «Калькулятор контейнеров». Следует отметить, что снимки – это не изображения, а дерево узлов в нашем отрендеренном Компоненте, сериализованном в файл в формате JSON.

Мы напишем тест для проверки пользовательского интерфейса нашего калькулятора. Для справки, вот как выглядит наш интерфейс прямо сейчас.

Модульное тестирование ReactJS - макет пользовательского интерфейса
Макет пользовательского интерфейса

Мы пишем тест для проверки того, что новые изменения не вносят непреднамеренных изменений в макет нашего Калькулятора. Мы добавляем следующий код в наш файл Calculator.test.js.

1
2
3
4
it('renders correctly', () => {
    const Calc = renderer.create(<Calculator />).toJSON();
    expect(Calc).toMatchSnapshot();
});

Давайте запустим этот тест с помощью команды npm test. Это первый раз, когда мы запускаем тест снимка, и это вызывает создание снимка и его сохранение в папке __snapshots___ в виде файла с именем Calculator.test.js.snap. Теперь, когда мы снова запустим тест, мы должны увидеть приведенный ниже вывод, поскольку все тесты пройдены.

Тестовые снимки пройдены

Чтобы проверить тест снимка, давайте внесем изменения в макет, чтобы имитировать непреднамеренное изменение или ошибку. А затем снова запустите тесты, чтобы увидеть результат ниже:

Сбой теста снимка

Такие тесты полезны для гарантии того, что непреднамеренные или побочные эффекты, приводящие к нарушению пользовательского интерфейса, обнаруживаются на ранней стадии. Далее мы рассмотрим, как мы можем использовать Jest для создания утверждений и имитации артефактов.

4. Насмешливый

Одной из важных частей модульного тестирования является изоляция или изоляция проверяемого блока. Так что на результаты теста не влияют внешние события или окружающая среда. Например, доступность серверных API, внешних ресурсов или других дочерних или родительских компонентов. Чтобы обеспечить такую ​​изоляцию, все среды тестирования предоставляют возможности для имитации артефактов. Так что наши тесты направлены на тестирование одного устройства за раз.

Например, здесь, когда мы тестируем компонент клавиатуры, который является дочерним по отношению к компоненту Калькулятор и отображает кнопки. Мы хотели бы изолировать компонент Keypad от обоих компонентов, чтобы мы могли проверить поведение компонента Keypad в одиночку. Как видно из кода, мы передаем обратные вызовы событий из калькулятора в клавиатуру и пересылаем обратные вызовы в обработчик нажатия кнопки, основываясь на его значении в клавиатуре. Давайте посмеемся над этими обратными вызовами и смоделируем нажатия кнопок, чтобы затем утверждать, что переданные обратные вызовы калькулятора вызываются с соответствующими аргументами. Наш тестовый файл теперь будет выглядеть так:

Keypad.test.js

01
02
03
04
05
06
07
08
09
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
import React from 'react';
import ReactDOM from 'react-dom';
import Keypad from './Keypad';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
 
Enzyme.configure({ adapter: new Adapter() });
 
// Mocked Callbacks
const handleKeypadClick = jest.fn(({ target }) => target.value);
const handleOperatorClick = jest.fn(({ target }) => target.value);
const handleEqualsClick = jest.fn(({ target }) => target.value);
const handleClearClick = jest.fn(({ target }) => target.value);
 
it('renders without crashing', () => {
    const div = document.createElement('div');
    ReactDOM.render(<Keypad />, div);
    ReactDOM.unmountComponentAtNode(div);
});
 
it('should call handleKeypadClick', () => {
    const keypad = shallow(<Keypad onEqualsClick={handleEqualsClick}
        onClearClick={handleClearClick}
        onKeyClick={handleKeypadClick}
        onOperatorClick={handleOperatorClick} />);
 
    keypad.find({ value: '2' }).simulate('click', { target: { value: 2 } });
 
    expect(handleKeypadClick).toBeCalledWith({ target: { value: 2 } });
 
});
it('should call handleEqualsClick', () => {
    const keypad = shallow(<Keypad onEqualsClick={handleEqualsClick}
        onClearClick={handleClearClick}
        onKeyClick={handleKeypadClick}
        onOperatorClick={handleOperatorClick} />);
 
    keypad.find({ value: '=' }).simulate('click', { target: { value: '=' } });
    expect(handleEqualsClick).toBeCalledWith({ target: { value: '=' } })
});
 
it('should call handleOperatorClick', () => {
    const keypad = shallow(<Keypad onEqualsClick={handleEqualsClick}
        onClearClick={handleClearClick}
        onKeyClick={handleKeypadClick}
        onOperatorClick={handleOperatorClick} />);
 
    keypad.find({ value: '+' }).simulate('click', { target: { value: '+' } });
    expect(handleOperatorClick).toBeCalledWith({ target: { value: '+' } })
});
 
it('should call handleClearClick', () => {
    const keypad = shallow(<Keypad onEqualsClick={handleEqualsClick}
        onClearClick={handleClearClick}
        onKeyClick={handleKeypadClick}
        onOperatorClick={handleOperatorClick} />);
 
    keypad.find({ value: 'Clear' }).simulate('click', { target: { value: 'Clear' } });
    expect(handleClearClick).toBeCalledWith({ target: { value: 'Clear' } })
});

5. Загрузите исходный код

Скачать
Вы можете скачать полный исходный код этого примера здесь: Пример модульного тестирования ReactJS