Тестирование React компонентов с Enzyme и Mocha

Зачем писать unit-тесты?

Представим себе обычный цикл разработки: получили задачу, решили её, протестировали, починили баги и выпустили версию. Затем получили баг-репорты и фиче-реквесты и приступили к новому циклу разработки. По завершению этого цикла нам снова надо будет проверить, что всё то, что было реализовано ранее, по прежнему работает — провести регрессионное тестирование. И проводить его надо будет для каждого нового цикла разработки. По мере разрастания проекта на это будет уходить всё больше и больше времени. А как происходит регрессионное тестирование в web-проектах? Кликаем мышкой по кнопкам и ссылкам. В каждом браузере, для каждой фичи, на каждом цикле разработки. Нашли баг, поправили, обновляем страницу и снова кликаем, кликаем, кликаем.

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

Касательно web-разработки есть ещё одно огромное преимущество — запуск тестов под разными платформами и браузерами. Больше нет нужды проверять дотошно, как этот кусок кода будет работать в msie, понравится ли он опере, а как к нему отнесется сафари. Достаточно написать тест, который проверит функциональность. Более того, эту работу можно распределить между обычными пользователями, хороший пример такой функциональности — testswarm.com.

Начало

Будем идти в ногу со временем и следовать последним стандартам ECMAScript или более известным, как ES6. Многие аспекты новой спецификации ES6 еще не имеют должной поддержки, поэтому мы будем использовать популярный транспайлер Babel, чтобы обеспечить работоспособность нашего кода в браузерах. Мы также будем использовать Webpack для сборки проекта. Babel и Webpack чрезвычайно гибкие и сложные инструменты, так что мы рассматрим только самые основные настройки.

Babel

Babel позволяет скомпилировать код на ES6 в ES5. Для начала установим все необходимые библиотеки:

$ npm install --save babel-core babel-loader babel-preset-react babel-preset-es2015 babel-preset-react babel-preset-stage-0

Нам также необходимо создать .babelrc файл в корневом каталоге приложения:

{
  "presets": ["react", "es2015", "stage-0"]
}

Настройка Webpack

Теперь, когда у нас установлен Babel, мы можем приступить к настройке Webpack.

Во-первых, установим сам Webpack и сервер для разработки:

$ npm install --save-dev webpack webpack-dev-server

Далее, нам нужно создать конфигурационный файл для Webpack. Он будет содержать параметры сборки.

webpack.config.js

const path = require('path');
const webpack = require('webpack');

// env
const buildDirectory = './dist/';

module.exports = {
  entry: './src/main.js',
  devServer: {
    hot: true,
    inline: true,
    port: 8080,
    historyApiFallback: true,
  },
  resolve: {
    extensions: ['', '.js'],
  },
  output: {
    path: path.resolve(buildDirectory),
    filename: 'app.js',
    publicPath: 'http://localhost:8080/dist',
  },
  externals: {
    'cheerio': 'window',
    'react/lib/ExecutionEnvironment': true,
    'react/lib/ReactContext': true,
  },
  module: {
    loaders: [{
      test: /\.js?$/,
      exclude: /node_modules/,
      loader: 'babel',
      query: {
        presets: ['react', 'es2015', 'stage-0'],
      },
    }],
  },
  plugins: [],
};

Index.html

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

<html>
<head>
  <title>Fun with react testing!</title>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, minimum-scale=1.0">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
</head>

<body>
  <div id="root">
    <h1>
      Loading!
    </h1>
  </div>
  <script src='http://localhost:8080/dist/app.js'></script>
</body>

</html>

Инструменты для тестирования

Последнее, что нам осталось, это установить Mocha и некоторые другие модули, чтобы запустить наши enzyme тесты.

$ npm install --save-dev mocha enzyme chai jsdom react-addons-test-utils

И, наконец, мы создадим установочный файл, который будет гарантировать, что мы можем проверить наши компоненты в реалистичной среде браузера, используя jsdom.

test/helpers/browser.js

require('babel-register')();

var jsdom = require('jsdom').jsdom;

var exposedProperties = ['window', 'navigator', 'document'];

global.document = jsdom('');
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
  if (typeof global[property] === 'undefined') {
    exposedProperties.push(property);
    global[property] = document.defaultView[property];
  }
});

global.navigator = {
  userAgent: 'node.js'
};

documentRef = document;

Вся установка зависимостей завершена. Мы можем запустить наши тесты с помощью команды npm test и запустить наш dev сервер командой npm run dev:hot, добавив эти сценарии к нашему package.json:

"scripts": {
    "test": "mocha -w test/helpers/browser.js test/*.spec.js",
    "dev:hot": "webpack-dev-server --hot --inline --progress --colors --watch --display-error-details --display-cached --content-base ./"
  }

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

Теперь, когда мы полностью закончили установку наших инструментов, мы можем начать писать тесты. Мы напишем очень простой компонент, который будет содержать изображение профиля из Gravatar, когда пользователь указывает электронную почту и нажимает кнопку "Fetch". Наши тесты должны отражать эти внешние требования и будет служить в качестве руководства, как только мы начинаем строить фактические компоненты. Весь смысл тестов в том, что когда все наши тесты проходят, мы можем быть абсолютно уверены в нашем коде.

Это пример теста для компонента avatar. avatar.spec.js

import React from 'react';
import { mount, shallow } from 'enzyme';
import { expect } from 'chai';

import Avatar from '../src/avatar';

describe('', function () {
  it('should have an image to display the gravatar', function () {
    const wrapper = shallow();
    expect(wrapper.find('img')).to.have.length(1);
  });

  it('should have props for email and src', function () {
    const wrapper = shallow();
    expect(wrapper.props().email).to.be.defined;
    expect(wrapper.props().src).to.be.defined;
  });
});

Обратите внимание на строчку, которая будет неоднократно повторятся во всех наших тестах:

const wrapper = shallow();

Shallow метод из enzyme позволяет "неглубоко" отрендерить компонент без дочерних.

Enzyme дает нам несколько способов рендеринга компонентов: shallow, mount, и static. Мы уже говорили о "неглубоком" рендеринге. Mount является "реальным" рендерингом, который будет на самом деле рендерить весь компонент. Если вы создаете умные React компоненты (не stateless ), вам будет необходимо использовать mount, чтобы сделать тестирование всего жизненного цикла методов компонента. Мы используем jsdom для выполнения рендеринга в браузеро подобном окружении, но вы могли бы так же легко запустить его в любом браузере.

Последний важный метод рендеринга enzyme static, который используется для анализа фактического HTML компонента и не будет рассматриваться в наших тестах. Методы shallow and mount дают нам много полезных методов, которые мы можем использовать, чтобы найти дочерние компоненты, проверить props, установить состояние, а также выполнять другие задачи тестирования. Так же мы будем использовать библиотеку Chai в наших тестах.

Теперь, когда мы знаем немного больше о том, как использовать фермент, мы можем закончить наши тесты:

email.spec.js

import React from 'react';
import { mount, shallow } from 'enzyme';
import { expect } from 'chai';

import Email from '../src/email';

describe('', function () {
  it('should have an input for the email', function () {
    const wrapper = shallow();
    expect(wrapper.find('input')).to.have.length(1);
  });

  it('should have a button', function () {
    const wrapper = shallow();
    expect(wrapper.find('button')).to.have.length(1);
  });

  it('should have props for handleEmailChange and fetchGravatar', function () {
    const wrapper = shallow();
    expect(wrapper.props().handleEmailChange).to.be.defined;
    expect(wrapper.props().fetchGravatar).to.be.defined;
  });
});

gravatar.spec.js

import React from 'react';
import { mount, shallow } from 'enzyme';
import { expect } from 'chai';
import md5 from 'md5';

import Gravatar from '../src/Gravatar';
import Avatar from '../src/Avatar';
import Email from '../src/Email';

describe('', () => {
	it('contains an  component', function () {
		const wrapper = mount();
		expect(wrapper.find(Avatar)).to.have.length(1);
	});

	it('contains an  component', function () {
		const wrapper = mount();
		expect(wrapper.find(Email)).to.have.length(1);
	});

	it('should have an initial email state', function () {
		const wrapper = mount();
		expect(wrapper.state().email).to.equal('someone@example.com');
	});

	it('should have an initial src state', function () {
		const wrapper = mount();
		expect(wrapper.state().src).to.equal('http://placehold.it/200x200');
	});

	it('should update the src state on clicking fetch', function () {
		const wrapper = mount();
		wrapper.setState({ email: 'hello@ifelse.io' });
		wrapper.find('button').simulate('click');
		expect(wrapper.state('email')).to.equal('hello@ifelse.io');
		expect(wrapper.state('src')).to.equal(`http://gravatar.com/avatar/${md5('hello@ifelse.io')}?s=200`);
	});
});

Теперь мы можем запустить наши тесты командой npm test и посмотреть результат выполенения:

Ну вот и все! Несложно, не правда ли? :) В рамках статьи я не стал разбирать создание самих компонентов, ссылку на них вы найдете ниже, но React я выбрал просто для примера, данные тесты подойдут и для любого другого фрэймворка.

Исходники можно найти здесь: https://github.com/mdenisov/react-mocha-enzyme-chai-example