Как компоненты React упрощают тестирование пользовательского интерфейса
Опубликовано: 2022-03-11Тестировать бэкенды легко. Вы выбираете язык по своему выбору, соединяете его с вашим любимым фреймворком, пишете несколько тестов и нажимаете «Выполнить». Ваша консоль говорит: «Ура! Оно работает!" Ваша служба непрерывной интеграции запускает ваши тесты при каждом нажатии, жизнь прекрасна.
Конечно, разработка через тестирование (TDD) поначалу кажется странной, но предсказуемая среда, несколько средств запуска тестов, инструменты тестирования, встроенные в фреймворки, и поддержка непрерывной интеграции облегчают жизнь. Пять лет назад я думал, что тесты — это решение всех проблем, с которыми я когда-либо сталкивался.
Затем Backbone стал большим.
Мы все перешли на интерфейс MVC. Наши тестируемые серверные части стали прославленными серверами баз данных. Наш самый сложный код переместился в браузер. И наши приложения больше не подлежали тестированию на практике.
Это потому, что тестирование внешнего кода и компонентов пользовательского интерфейса довольно сложно.
Это не так уж плохо, если все, что мы хотим, это проверить, что наши модели ведут себя хорошо. Или, что вызов функции изменит правильное значение. Все, что нам нужно сделать для модульного тестирования React, это:
- Пишите хорошо сформированные, изолированные модули.
- Используйте тесты Jasmine или Mocha (или что-то еще) для запуска функций.
- Используйте средство запуска тестов, например Karma или Chutzpah.
Вот и все. Наш код проходит модульное тестирование.
Раньше считалось, что выполнение интерфейсных тестов было сложной задачей. У каждого фреймворка были свои идеи, и в большинстве случаев у вас получалось окно браузера, которое вы вручную обновляли каждый раз, когда хотели запустить тесты. Конечно, вы всегда будете забывать. По крайней мере, я знаю, что знал.
В 2012 году Войта Джина выпустила раннер Karma (в то время называвшийся Testacular). С Karma интерфейсное тестирование становится полноценным участником цепочки инструментов. Наши тесты React запускаются в терминале или на сервере непрерывной интеграции, они перезапускаются при изменении файла, и мы даже можем тестировать наш код в нескольких браузерах одновременно.
Чего еще мы могли бы пожелать? Ну, чтобы на самом деле протестировать наш интерфейсный код.
Фронтенд-тестирование требует больше, чем просто модульные тесты
Модульное тестирование — это прекрасно: это лучший способ убедиться, что алгоритм каждый раз работает правильно, или проверить нашу логику проверки ввода, или преобразование данных, или любую другую изолированную операцию. Модульное тестирование идеально подходит для основ.
Но интерфейсный код не предназначен для манипулирования данными. Речь идет о пользовательских событиях и отображении правильных представлений в нужное время. Фронтенды — это про пользователей.
Вот что мы хотим уметь делать:
- Протестируйте пользовательские события React
- Проверьте реакцию на эти события
- Убедитесь, что правильные вещи отображаются в нужное время
- Запуск тестов во многих браузерах
- Повторно запустить тесты на изменения файлов
- Работайте с системами непрерывной интеграции, такими как Travis
За десять лет, что я этим занимался, я не нашел достойного способа протестировать взаимодействие с пользователем и визуализацию, пока не начал ковыряться в React.
Модульное тестирование React: компоненты пользовательского интерфейса
React — самый простой способ достичь этих целей. Отчасти из-за того, что это заставляет нас создавать приложения с использованием тестируемых шаблонов, отчасти потому, что существуют фантастические утилиты для тестирования React.
Если вы никогда раньше не использовали React, вам следует ознакомиться с моей книгой React+d3.js . Он ориентирован на визуализацию, но мне сказали, что это «потрясающее легкое введение» в React.
React заставляет нас создавать все как «компоненты». Компоненты React можно рассматривать как виджеты или фрагменты HTML с некоторой логикой. Они следуют многим из лучших принципов функционального программирования, за исключением того, что они являются объектами.
Например, при одном и том же наборе параметров компонент React всегда будет отображать один и тот же результат. Неважно, сколько раз он рендерится, неважно, кто его рендерит, неважно, где мы размещаем вывод. Всегда одно и то же. В результате нам не нужно выполнять сложные строительные леса для тестирования компонентов React. Они заботятся только о своих свойствах, не требуя отслеживания глобальных переменных и объектов конфигурации.
Мы достигаем этого в значительной степени, избегая состояния. Вы бы назвали это ссылочной прозрачностью в функциональном программировании. Я не думаю, что в React для этого есть специальное название, но официальные документы рекомендуют избегать использования состояния, насколько это возможно.
Когда дело доходит до тестирования взаимодействия с пользователем, React предлагает нам события, связанные с обратными вызовами функций. Легко настроить тестовых шпионов и убедиться, что событие клика вызывает правильную функцию. А поскольку компоненты React визуализируются сами по себе, мы можем просто инициировать событие щелчка и проверить HTML на наличие изменений. Это работает, потому что компонент React заботится только о себе. Щелчок здесь ничего не меняет там . Нам никогда не придется иметь дело с набором обработчиков событий, только с четко определенными вызовами функций.
Да, и поскольку React — это волшебство, нам не нужно беспокоиться о DOM. React использует так называемый виртуальный DOM для рендеринга компонентов в переменную JavaScript. И ссылка на виртуальный DOM — это все, что нам действительно нужно для тестирования компонентов React.
Это довольно мило.
TestUtils
React
React поставляется с набором встроенных TestUtils
. Есть даже рекомендуемый тест-раннер под названием Jest, но он мне не нравится. Я объясню, почему в немного. Во-первых, TestUtils
.
Мы получаем их, делая что-то вроде require('react/addons').addons.TestUtils
. Это наша отправная точка для тестирования взаимодействия с пользователем и проверки результатов.
React TestUtils
позволяет отображать компонент React, помещая его DOM в переменную, а не вставляя его на страницу. Например, чтобы визуализировать компонент React, мы должны сделать что-то вроде этого:
var component = TestUtils.renderIntoDocument( <MyComponent /> );
Затем мы можем использовать TestUtils
, чтобы проверить, все ли дочерние элементы были обработаны. Что-то вроде этого:
var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );
findRenderedDOMComponentWithTag
будет делать то, на что это похоже: пройтись по дочерним элементам, найти искомый компонент и вернуть его. Возвращаемое значение будет вести себя как компонент React.
Затем мы можем использовать getDOMNode()
для доступа к необработанному элементу DOM и проверки его значений. Чтобы проверить, что тег h1
в компоненте говорит «Заголовок» , мы напишем это:
expect(h1.getDOMNode().textContent) .toEqual("A title");
В совокупности полный тест будет выглядеть так:
it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });
Самое интересное, что TestUtils также позволяет нам запускать пользовательские события. Для события клика мы бы написали что-то вроде этого:
var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);
Это имитирует щелчок и запускает любые потенциальные прослушиватели, которые должны быть методами компонентов, которые изменяют вывод, состояние или и то, и другое. Эти слушатели могут при необходимости вызывать функцию родительского компонента.
Все случаи легко проверить: измененное состояние находится в component.state
, мы можем получить доступ к выходным данным с помощью обычных функций DOM и вызовов функций с помощью шпионов.
Почему не шутка?
Официальная документация React рекомендует использовать https://facebook.github.io/jest/ в качестве средства запуска тестов и среды тестирования React. Jest построен на Jasmine и использует тот же синтаксис. Вдобавок ко всему, что вы получаете от Jasmine, Jest также имитирует все, кроме тестируемого компонента. Это фантастика в теории, но я нахожу это раздражающим. Все, что мы еще не реализовали или что происходит из другой части кодовой базы, просто undefined
. Хотя во многих случаях это нормально, это может привести к незаметному сбою ошибок.
Например, у меня возникли проблемы с тестированием события click. Что бы я ни пытался, он просто не вызывал своего слушателя. Затем я понял, что Jest высмеивал эту функцию, и она никогда не говорила мне об этом.
Но самым большим недостатком Jest, безусловно, было то, что у него не было режима просмотра для автоматического тестирования новых изменений. Мы могли запустить его один раз, получить результаты тестов, и все. (Мне нравится запускать тесты в фоновом режиме, пока я работаю. В противном случае я забываю их запускать.) В настоящее время это больше не проблема.
О, и Jest не поддерживает запуск тестов React в нескольких браузерах. Это меньше проблема, чем раньше, но я чувствую, что это важная функция для тех редких случаев, когда гейзенбаг возникает только в определенной версии Chrome…
Примечание редактора: с момента написания этой статьи Jest существенно улучшился. Вы можете прочитать наш более свежий учебник React Unit Testing Using Enzyme и Jest и решить для себя, подходит ли тестирование Jest в настоящее время.
Тестирование React: интегрированный пример
В любом случае, мы видели, как теоретически должен работать хороший интерфейсный тест React. Давайте попробуем это сделать на коротком примере.
Мы собираемся визуализировать различные способы генерации случайных чисел с помощью компонента диаграммы рассеяния, созданного с помощью React и d3.js. Код и его демонстрация также находятся на Github.
Мы собираемся использовать Karma в качестве средства запуска тестов, Mocha в качестве среды тестирования и Webpack в качестве загрузчика модулей.
Установка
Наши исходные файлы будут находиться в каталоге <root>/src
, а тесты мы поместим в <root>/src/__tests__
. Идея состоит в том, что мы можем поместить в src
несколько каталогов, по одному для каждого основного компонента и каждый со своими тестовыми файлами. Такое объединение исходного кода и тестовых файлов упрощает повторное использование компонентов React в разных проектах.

Имея структуру каталогов, мы можем установить такие зависимости:
$ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect
Если что-то не удается установить, попробуйте повторно запустить эту часть установки. Иногда NPM дает сбой, который исчезает при повторном запуске.
Наш файл package.json
должен выглядеть так, когда мы закончим:
// package.json { "name": "react-testing-example", "description": "A sample project to investigate testing options with ReactJS", "scripts": { "test": "karma start" }, // ... "homepage": "https://github.com/Swizec/react-testing-example", "devDependencies": { "babel-core": "^5.2.17", "babel-loader": "^5.0.0", "d3": "^3.5.5", "expect": "^1.6.0", "jsx-loader": "^0.13.2", "karma": "^0.12.31", "karma-chrome-launcher": "^0.1.10", "karma-cli": "0.0.4", "karma-mocha": "^0.1.10", "karma-sourcemap-loader": "^0.3.4", "karma-webpack": "^1.5.1", "mocha": "^2.2.4", "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-tools": "^0.13.3", "webpack": "^1.9.4", "webpack-dev-server": "^1.8.2" } }
После некоторой настройки мы сможем запускать тесты либо с помощью npm test
либо с помощью karma start
.
Конфигурация
В конфигурации не так много. Мы должны убедиться, что Webpack знает, как найти наш код, и что Karma знает, как запускать тесты.
Мы помещаем две строки JavaScript в файл ./tests.webpack.js
, чтобы помочь Karma и Webpack работать вместе:
// tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);
Это говорит Webpack считать все, что имеет суффикс -test
, частью набора тестов.
Настройка Karma требует немного больше работы:
// karma.conf.js var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], singleRun: true, frameworks: ['mocha'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} ] }, watch: true }, webpackServer: { noInfo: true } }); };
Большинство этих строк взяты из конфигурации Karma по умолчанию. Мы использовали browsers
, чтобы сказать, что тесты должны выполняться в Chrome, frameworks
, чтобы указать, какую инфраструктуру тестирования мы используем, и singleRun
, чтобы тесты запускались только один раз по умолчанию. Вы можете поддерживать работу кармы в фоновом режиме с помощью karma start --no-single-run
.
Эти три очевидны. Материал Webpack более интересен.
Поскольку Webpack обрабатывает дерево зависимостей нашего кода, нам не нужно указывать все наши файлы в массиве files
. Нам нужен только tests.webpack.js
, для которого потом нужны все необходимые файлы.
Мы используем настройку webpack
, чтобы указать Webpack, что делать. В обычной среде эта часть будет храниться в файле webpack.config.js
.
Мы также говорим Webpack использовать babel-loader
для наших JavaScript. Это дает нам все новые модные функции из ECMAScript2015 и JSX React.
В конфигурации webpackServer
мы указываем Webpack не печатать отладочную информацию. Это только испортило бы наш тестовый результат.
Компонент React и тест
С запущенным набором тестов все остальное просто. Нам нужно сделать компонент, который принимает массив случайных координат и создает элемент <svg>
с кучей точек.
Следуя рекомендациям по тестированию React — то есть стандартной практике TDD — мы сначала напишем тест, а затем сам компонент React. Начнем с файла ванильных тестов в src/__tests__/
:
// ScatterPlot-test.jsx var React = require('react/addons'), TestUtils = React.addons.TestUtils, expect = require('expect'), ScatterPlot = require('../ScatterPlot.jsx'); var d3 = require('d3'); describe('ScatterPlot', function () { var normal = d3.random.normal(1, 1), mockData = d3.range(5).map(function () { return {x: normal(), y: normal()}; }); });
Сначала нам потребуется React, его TestUtils, expect
, ожидаемая библиотека и код, который мы тестируем. Затем мы создаем новый набор тестов с describe
и создаем некоторые случайные данные.
Для нашего первого теста давайте удостоверимся, что ScatterPlot
отображает заголовок. Наш тест проходит внутри блока describe
:
// ScatterPlot-test.jsx it("renders an h1", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( scatterplot, 'h1' ); expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); });
Большинство тестов будут следовать одному и тому же шаблону:
- Оказывать.
- Найдите конкретный узел.
- Проверьте содержимое.
Как мы видели ранее, renderIntoDocument
визуализирует наш компонент, findRenderedDOMComponentWithTag
находит определенную часть, которую мы тестируем, а getDOMNode
предоставляет нам необработанный доступ к DOM.
Сначала наш тест провалится. Чтобы это прошло, мы должны написать компонент, который отображает тег заголовка:
var React = require('react/addons'); var d3 = require('d3'); var ScatterPlot = React.createClass({ render: function () { return ( <div> <h1>This is a random scatterplot</h1> </div> ); } }); module.exports = ScatterPlot;
Вот и все. Компонент ScatterPlot
отображает <div>
с <h1>
, содержащим ожидаемый текст, и наш тест будет пройден. Да, это длиннее, чем просто HTML, но потерпите меня.
Нарисуйте остальную часть совы
Вы можете увидеть остальную часть нашего примера на GitHub, как упоминалось выше. Мы не будем описывать это шаг за шагом в этой статье, но общий процесс такой же, как описано выше. Однако я хочу показать вам более интересный тест. Тест, который гарантирует, что все точки данных отображаются на графике:
// ScatterPlot-test.jsx it("renders a circle for each datapoint", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot data={mockData} /> ); var circles = TestUtils.scryRenderedDOMComponentsWithTag( scatterplot, 'circle' ); expect(circles.length).toEqual(5); });
Так же, как раньше. Рендерим, находим узлы, проверяем результат. Самое интересное здесь — отрисовка этих DOM-узлов. Мы добавляем немного магии ScatterPlot
в компонент ScatterPlot, например:
// ScatterPlot.jsx componentWillMount: function () { this.yScale = d3.scale.linear(); this.xScale = d3.scale.linear(); this.update_d3(this.props); }, componentWillReceiveProps: function (newProps) { this.update_d3(newProps); }, update_d3: function (props) { this.yScale .domain([d3.min(props.data, function (d) { return dy; }), d3.max(props.data, function (d) { return dy; })]) .range([props.point_r, Number(props.height-props.point_r)]); this.xScale .domain([d3.min(props.data, function (d) { return dx; }), d3.max(props.data, function (d) { return dx; })]) .range([props.point_r, Number(props.width-props.point_r)]); }, ...
Мы используем componentWillMount
, чтобы настроить пустые шкалы d3 для доменов X и Y , и componentWillReceiveProps
, чтобы убедиться, что они обновляются, когда что-то меняется. Затем update_d3
обязательно устанавливает domain
и range
для обеих шкал.
Мы будем использовать две шкалы для перевода между случайными значениями в нашем наборе данных и позициями на картинке. Большинство генераторов случайных чисел возвращают числа в диапазоне [0,1] , что слишком мало, чтобы рассматривать их как пиксели.
Затем мы добавляем точки в метод рендеринга нашего компонента:
// ScatterPlot.jsx render: function () { return ( <div> <h1>This is a random scatterplot</h1> <svg width={this.props.width} height={this.props.height}> {this.props.data.map(function (pos, i) { var key = "circle-"+i; return ( <circle key={key} cx={this.xScale(pos.x)} cy={this.yScale(pos.y)} r={this.props.point_r} /> ); }.bind(this))}; </svg> </div> ); }
Этот код проходит через массив this.props.data
и добавляет элемент <circle>
для каждой точки данных. Простой.
Если вы хотите узнать больше об объединении React и d3.js для создания компонентов визуализации данных, это еще одна веская причина прочитать мою книгу React+d3.js .
Автоматизированное тестирование компонентов React: проще, чем кажется
Это все, что нам нужно знать о написании тестируемых интерфейсных компонентов с помощью React. Чтобы увидеть больше кода, тестирующего компоненты React, ознакомьтесь с тестовой базой кода React на Github, как упоминалось выше.
Мы узнали, что:
- React заставляет нас использовать модули и инкапсулировать.
- Это упрощает автоматизацию тестирования пользовательского интерфейса React.
- Модульных тестов недостаточно для интерфейсов.
- Карма — отличный тест-раннер.
- Jest имеет потенциал, но еще не совсем там. (Или, может быть, сейчас это так.)
Если вам понравилась эта статья, подпишитесь на меня в Твиттере и оставьте комментарий ниже. Спасибо за прочтение и удачного тестирования React!