Если вы используете Webpack в своем проекте, вы, вероятно, захотите использовать файл bundle.js для тестирования компонентов React во время цикла TDD. Пакет mocha-webpack упрощает эту задачу:

npm i mocha mocha-webpack enzyme jsdom sinon chai --save-dev

Нам нужен новый файл с именем mocha-webpack.opts в нашем корневом каталоге, чтобы сохранить наши параметры mocha-webpack:

--colors
--require ignore-styles
--require babel-core/register
--require jsdom-global/register

Наш компонент React:

'use strict'
import { connect } from 'react-redux'
import { render } from 'react-dom'
import * as TestsActionCreators from '../actions/tests'
import React, { Component, PropTypes } from 'react'
import { Link, browserHistory, withRouter} from 'react-router'
import { Button, Modal } from 'react-bootstrap'
// export for unconnected component (for mocha tests)
export class QuestionsComponent extends Component {
  constructor(props) {
      super(props);
      this.state = {
          questions:   [],
          test_id:     this.props.routeParams.test_id
         }
}
      //Test mocha:  console.log('This props >>>>>>' + JSON.stringify(this.props));
  }
/**
   * Load test data and questions
   **/
  componentWillMount() {
    if ( ! this.props.OneTestArrayProp.length ) {
      let action = TestsActionCreators.fetchOneTest( this.props.routeParams.test_id );
      this.props.dispatch(action);
    }
  }
render() {
    return (
      <div className="container_div">
          {this.props.QuestionsArrayProp.map((q, i) =>
            <div key={i} className="questions_div">
              <div><b>{i+1}.- Question</b>: {q.question} -- {q.id} </div>
            </div>
        )}
      </div>
      { this.props.children }
      </div>
    )
  }
}
QuestionsComponent.propTypes = {
  QuestionsArrayProp: PropTypes.array
}
QuestionsComponent.defaultProps = {
  QuestionsArrayProp: []
}
const mapStateToProps = (state) => {
  return {
    QuestionsArrayProp: state.rootReducer.tests_rdcr.QuestionsArrayProp
  }
}
export default withRouter(connect(mapStateToProps)(QuestionsComponent));

Наш файл webpack для использования в тестовой среде:

// file: webpack.testing.config.js
path = require('path');
var webpack = require('webpack');
module.exports = {
  entry: {
    app: ["./entry.jsx"]
  },
  target: 'web',
  debug: 'devel',
  devtool: 'eval',
  output: {
    path: path.resolve(__dirname, "build"),
    publicPath: "/build/",
    filename: "bundle.js"
  },
  resolve: {
    extensions: ['', '.js', '.jsx']
  },
  module: {
    loaders:  [
                {test: /sinon.js$/, loader: "imports?define=>false" },
                {test: /\.jsx?$/, loader: 'babel', exclude: /node_modules/,  query: {
                                 cacheDirectory: true,
                                 presets: ["react", "es2015", "stage-0"]
                                }, include: path.app
                },
                { test: /\.css$/, loader: 'style-loader!css-loader'},
                { test: /\.js$/, loader: 'babel', exclude: /node_modules/, query: {
                        presets: ["es2015", "stage-0"]
                }},
                { test: /\.less$/, loader: 'style!css!less' },
                { test: /\.scss$/, loader: 'style!css!sass' },
                { test: /\.json$/, loader: "json-loader" },
                { test: /\.woff(2)?(\?v=[0-9].[0-9].[0-9])?$/, loader: "url-loader?mimetype=application/font-woff" },
                { test: /\.(ttf|eot|svg)(\?v=[0-9].[0-9].[0-9])?$/, loader: "file-loader?name=[name].[ext]" },
                { test: /\.gif$/, loader: "url-loader?mimetype=image/png" },
                { test: /bootstrap-sass\/assets\/javascripts\//, loader: 'imports?jQuery=jquery' } // Bootstrap 3
              ]
  },
   externals: [
     {
       'isomorphic-fetch': {
         root: 'isomorphic-fetch',
         commonjs2: 'isomorphic-fetch',
         commonjs: 'isomorphic-fetch',
         amd: 'isomorphic-fetch'
       },
       'cheerio': 'window',
       'react/addons': true,
       'react/lib/ExecutionEnvironment': true,
       'react/lib/ReactContext': true
     }
   ],
  node: {
     fs: "empty"
  },
  plugins: [
       new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery"
       }),
       new webpack.IgnorePlugin(/ReactContext|react\/addons/),
       new webpack.optimize.DedupePlugin()
    ]
};

Как вы, наверное, уже знаете, одним из преимуществ mocha является то, что у него нет собственного DOM, поэтому мы должны загрузить DOM с помощью помощника:

// file: __tests__/helpers/browser.js
var baseDOM = '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body></body></html>';
var jsdom = require('jsdom').jsdom;
global.document = jsdom(baseDOM);
global.window   = document.defaultView;
if ( global.self != null) {
  console.log(' global.self >>>>> ' + global.self);
} else {
  global.self = global.this;
}
global.navigator = {
  userAgent: 'node.js'
};

Наш файл mocha для тестирования класса React выглядит так:

'use strict';
//  Mocha TEST
// file: __tests__/components/QuestionComponent.spec.js
import ReactDOM from 'react-dom';
import React from 'react';
import { expect } from 'chai';
import { mount, shallow, render } from 'enzyme';
import { QuestionsComponent } from '../../components/QuestionsComponent';
import sinon from 'sinon';
var dispatch = function() {
   console.log('>>>>>>>> Mocking dispatch ');
 };
var props = {
   dispatch: dispatch,
   routeParams: {test_id: 1},
   tests_rdcr: {}
  };
describe('QuestionsComponent', function () {
it('QuestionsComponent calls componentWillMount', () => {
     sinon.spy(QuestionsComponent.prototype, 'componentWillMount');
     const enzymeWrapper = mount(<QuestionsComponent {...props} />);
     expect(QuestionsComponent.prototype.componentWillMount.calledOnce).to.equal(true);
   });
it("QuestionsComponent does not render questions_div", function() {
     expect(shallow(<QuestionsComponent {...props} />).contains(<div className="questions_div" />)).to.equal(false);
   });
it("QuestionsComponent does not render questions_div", function() {
      var answers = [{id:1, answer: 'answer uno',  correct: true,  active: true, question_id: 1},
                     {id:2, answer: 'answer dos ',  correct: true,  active: true, question_id: 1},
                     {id:3, answer: 'answer tres',  correct: true,  active: true, question_id: 1}
                    ];
props['QuestionsArrayProp'] = answers;
    expect(mount(<QuestionsComponent {...props} />).find('.questions_div').length).to.equal(3);
  });
});

Теперь одна из проблем с реальным компонентом React заключается в том, что компонент переплетен с Redux и React-Router, и нам нужно изолировать компонент, чтобы выполнить модульное тестирование. Добиться этого действия на удивление легко. Обычно в рабочей среде мы импортируем компонент следующим образом:

import QuestionsComponent from './components/QuestionsComponent'

Отсутствие фигурных скобок вокруг имени компонента означает, что мы импортируем экспортируемый элемент по умолчанию, id est:

export default withRouter(connect(mapStateToProps)(QuestionsComponent));

Но если мы добавим оператор экспорта не по умолчанию в наш класс React:

// export for unconnected component (for mocha tests)
export class QuestionsComponent extends Component {

а затем мы импортируем с помощью фигурных скобок:

import { QuestionsComponent } from './components/QuestionsComponent'

это означает, что теперь мы можем загрузить и протестировать компонент, изолированный от Redux и React-Router. Довольно круто, ха?

Теперь мы можем протестировать наш компонент:

./node_modules/mocha-webpack/bin/mocha-webpack --opts ./mocha-webpack.opts --webpack-config ./webpack.testing.config.js -r __tests__/helpers/browser.js __tests__/components/QuestionComponent.spec.js

Если все в порядке, вы должны увидеть что-то вроде: