Разработка React.js через тестирование: Модульное тестирование React.js с помощью Enzyme и Jest
Опубликовано: 2022-03-11По словам Майкла Фезерса, любой фрагмент кода, в котором нет тестов, считается устаревшим кодом. Таким образом, один из лучших способов избежать создания устаревшего кода — использовать разработку через тестирование (TDD).
Хотя существует множество инструментов для модульного тестирования JavaScript и React.js, в этом посте мы будем использовать Jest и Enzyme для создания компонента React.js с базовой функциональностью с использованием TDD.
Зачем использовать TDD для создания компонента React.js?
TDD дает много преимуществ вашему коду — одно из преимуществ высокого покрытия тестами заключается в том, что он позволяет легко проводить рефакторинг кода, сохраняя при этом чистоту и функциональность кода.
Если вы уже создавали компонент React.js, вы поняли, что код может расти очень быстро. Он заполнен множеством сложных условий, вызванных операторами, связанными с изменениями состояния и вызовами службы.
Каждый компонент, в котором отсутствуют модульные тесты, имеет устаревший код, который становится трудно поддерживать. Мы могли бы добавить модульные тесты после того, как создадим производственный код. Однако мы можем рискнуть упустить из виду некоторые сценарии, которые должны были быть протестированы. Создавая сначала тесты, у нас больше шансов охватить каждый логический сценарий в нашем компоненте, что упростит его рефакторинг и поддержку.
Как мы проводим модульное тестирование компонента React.js?
Есть много стратегий, которые мы можем использовать для тестирования компонента React.js:
- Мы можем проверить, что конкретная функция в
props
была вызвана при отправке определенного события. - Мы также можем получить результат функции
render
с учетом текущего состояния компонента и сопоставить его с предопределенным макетом. - Мы даже можем проверить, соответствует ли число потомков компонента ожидаемому количеству.
Чтобы использовать эти стратегии, мы будем использовать два инструмента, которые пригодятся для работы с тестами в React.js: Jest и Enzyme.
Использование Jest для создания модульных тестов
Jest — это среда тестирования с открытым исходным кодом, созданная Facebook, которая отлично интегрируется с React.js. Он включает в себя инструмент командной строки для выполнения тестов, аналогичный тому, что предлагают Jasmine и Mocha. Это также позволяет нам создавать фиктивные функции практически с нулевой конфигурацией и предоставляет действительно хороший набор сопоставителей, который упрощает чтение утверждений.
Кроме того, он предлагает очень хорошую функцию под названием «тестирование моментальных снимков», которая помогает нам проверять и проверять результат рендеринга компонента. Мы будем использовать тестирование моментальных снимков, чтобы захватить дерево компонентов и сохранить его в файл, который мы можем использовать для сравнения с деревом рендеринга (или с тем, что мы передаем функции expect
в качестве первого аргумента).
Использование Enzyme для монтирования компонентов React.js
Enzyme предоставляет механизм для монтирования и обхода деревьев компонентов React.js. Это поможет нам получить доступ к его собственным свойствам и состоянию, а также к его дочерним реквизитам для запуска наших утверждений.
Enzyme предлагает две основные функции для монтажа компонентов: shallow
и mount
. shallow
функция загружает в память только корневой компонент, тогда как mount
загружает полное дерево DOM.
Мы собираемся объединить Enzyme и Jest, чтобы смонтировать компонент React.js и запустить над ним утверждения.
Настройка нашей среды
Вы можете взглянуть на этот репозиторий, в котором есть базовая конфигурация для запуска этого примера.
Мы используем следующие версии:
{ "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }
Создание компонента React.js с использованием TDD
Первый шаг — создать неудачный тест, который попытается отобразить компонент React.js, используя мелкую функцию фермента.
// MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });
После запуска теста получаем следующую ошибку:
ReferenceError: MyComponent is not defined.
Затем мы создаем компонент, предоставляющий базовый синтаксис для прохождения теста.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }
На следующем шаге мы убедимся, что наш компонент отображает предопределенный макет пользовательского интерфейса, используя функцию toMatchSnapshot
из Jest.
После вызова этого метода Jest автоматически создает файл моментального снимка с именем [testFileName].snap
, который добавляется в папку __snapshots__
.
Этот файл представляет макет пользовательского интерфейса, который мы ожидаем от рендеринга нашего компонента.
Однако, учитывая, что мы пытаемся выполнить чистый TDD, мы должны сначала создать этот файл, а затем вызвать функцию toMatchSnapshot
, чтобы сделать тест непройденным.
Это может показаться немного запутанным, учитывая, что мы не знаем, какой формат Jest использует для представления этого макета.
У вас может возникнуть соблазн сначала выполнить функцию toMatchSnapshot
и увидеть результат в файле моментального снимка, и это допустимый вариант. Однако, если мы действительно хотим использовать чистый TDD, нам нужно узнать, как устроены файлы моментальных снимков.
Файл моментального снимка содержит макет, соответствующий названию теста. Это означает, что если наш тест имеет такой вид:
desc("ComponentA" () => { it("should do something", () => { … } });
Мы должны указать это в разделе экспорта: Component A should do something 1
.
Подробнее о тестировании снэпшотов можно прочитать здесь.
Итак, сначала мы создаем файл MyComponent.test.js.snap
.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;
Затем мы создаем модульный тест, который проверит, соответствует ли снимок дочерним элементам компонента.

// MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...
components.getElements
можно рассматривать как результат метода рендеринга.
Мы передаем эти элементы expect
методу, чтобы запустить проверку файла моментального снимка.
После выполнения теста получаем следующую ошибку:
Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []
Jest сообщает нам, что результат из component.getElements
не соответствует снимку. Итак, мы делаем этот тест пройденным, добавляя элемент ввода в MyComponent
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }
Следующим шагом является добавление функциональности к input
путем выполнения функции при изменении ее значения. Мы делаем это, указав функцию в onChange
.
Сначала нам нужно изменить снимок, чтобы тест провалился.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;
Недостатком изменения снимка в первую очередь является то, что важен порядок свойств (или атрибутов).
Jest отсортирует реквизиты, полученные в функции expect
, в алфавитном порядке, прежде чем сверять их со снимком. Таким образом, мы должны указать их в этом порядке.
После выполнения теста получаем следующую ошибку:
Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]
Чтобы этот тест прошел, мы можем просто предоставить пустую функцию onChange
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }
Затем мы убеждаемся, что состояние компонента изменяется после отправки события onChange
.
Для этого мы создаем новый модульный тест, который будет вызывать функцию onChange
на входе, передавая событие , чтобы имитировать реальное событие в пользовательском интерфейсе.
Затем мы проверяем, что состояние компонента содержит ключ с именем input
.
// MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });
Теперь мы получаем следующую ошибку.
Expected value to be defined, instead received undefined
Это указывает на то, что компонент не имеет свойства в состоянии с именем input
.
Мы делаем тест пройденным, установив эту запись в состоянии компонента.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }
Затем нам нужно убедиться, что значение установлено в новой записи состояния. Мы получим это значение из события.
Итак, давайте создадим тест, который удостоверится, что состояние содержит это значение.
// MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""
Наконец, мы проходим этот тест, получая значение из события и устанавливая его в качестве входного значения.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }
Убедившись, что все тесты пройдены, мы можем реорганизовать наш код.
Мы можем извлечь функцию, переданную в onChange
, в новую функцию с именем updateState
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }
Теперь у нас есть простой компонент React.js, созданный с помощью TDD.
Резюме
В этом примере мы попытались использовать чистый TDD, следуя каждому шагу, написав как можно меньше кода, чтобы не пройти тесты.
Некоторые из шагов могут показаться ненужными, и у нас может возникнуть соблазн пропустить их. Однако всякий раз, когда мы пропускаем какой-либо шаг, мы в конечном итоге используем менее чистую версию TDD.
Использование менее строгого процесса TDD также допустимо и может работать нормально.
Я рекомендую вам не пропускать какие-либо шаги и не расстраиваться, если вам будет трудно. TDD — техника, которую нелегко освоить, но она определенно стоит того.
Если вам интересно узнать больше о TDD и связанной с ним разработке, основанной на поведении (BDD), прочитайте «Ваш босс не оценит TDD» коллеги по Toptaler Райана Уилкокса.