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

Мы начинаем отсюда:

npm install --save-dev webpack babel-core babel-loader babel-plugin-rewire babel-preset-stage-1 babel-preset-react babel-preset-es2015 babel-polyfill babel-plugin-transform-react-remove-prop-types json-loader react-addons-test-utils webpack tape tap-spec karma karma-webpack karma-chrome-launcher karma-cli karma-htmlfile-reporter karma-sourcemap-loader karma-tap karma-tap-pretty-reporter enzyme cross-env

Ого, это было много… Вы также можете установить глобально webpack и karma-cli.

Карма и настройка

Начнем со скучной части. Karma — наш выбор для тестировщика. Он может имитировать различные среды браузера. Если вам нужны более глубокие знания, есть много статей о карме. Я бы не стал здесь вдаваться в подробности. Проблема в том, что его сложно настроить. Вот пример настройки. Это уже решает многие проблемы, но всегда может возникнуть больше. Тогда вам следует знать, как искать в гугле, иначе вы заблудились. Без шуток! В коде есть комментарии, а там, где их нет, свойства говорят сами за себя. Вы можете создать файл с именами karma.conf.js и test.webpack.js в корневом каталоге и скопировать и вставить это содержимое.

карма.conf.js

const webpack = require('webpack')
const path = require('path')
const debug = (process.argv.slice(3)).some(argv => argv === '--debug')

module.exports = function (config) {
    config.set({
        browsers: ['Chrome'],
        // Change this if you want to debug
        singleRun: !debug,
        autoWatch: false,
        // Tap framework for console output. This is not Tape.
        frameworks: ['tap'],
        browserDisconnectTimeout: 100000,
        browserNoActivityTimeout: 100000,
        // In this file all other files are required
        files: [
            'tests.webpack.js'
        ],
        preprocessors: {
            'tests.webpack.js': ['webpack', 'sourcemap']
        },
        reporters: ['tap-pretty', 'html'],
        // You can choose which tap reporter suits
        tapReporter: {
            prettifier: 'tap-spec',
            sepparator: true
        },
        // Remove anoying LOG if not in debug mode
        client: {
            captureConsole: debug
        },
        htmlReporter: {
            outputFile: 'tests/test-results.html',
            pageTitle: 'Tests',
            groupSuites: true,
            useCompactStyle: true,
            useLegacyStyle: true
        },
        webpack: {
            devtool: 'inline-source-map',
            module: {
                loaders: [
                    {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'},
                    {test: /\.json$/, loader: 'json-loader'}
                ]
            },
            // hot fix about Cannot resolve module 'fs' error
            node: {
                fs: 'empty'
            },
            resolve: {
                extensions: ['', '.js', '.json']
            },
            // Specify dependencies that shouldn’t be resolved by webpack,
            // but should become dependencies of the resulting bundle.
            externals: {
                'jsdom': 'window',
                'cheerio': 'window',
                'react/lib/ReactContext': 'window',
                'react/lib/ExecutionEnvironment': true,
                'react/addons': true
            },
            // Fixes the 'two copy of React' problem
            alias: {
                'react': path.join(__dirname, 'node_modules', 'react')
            },
            // Custom constants in code
            plugins: [
                new webpack.DefinePlugin({
                    'process.env': {
                        NODE_ENV: JSON.stringify('test'),
                        API_URL: JSON.stringify('http://localhost:3000'),
                        HOST_URL: JSON.stringify('/')
                    }
                })
            ]
        },
        webpackServer: {
            noInfo: true
        }
    })
}

Скрипты package.json:

"test": "cross-env NODE_ENV=test karma start",
"test:debug": "cross-env NODE_ENV=test karma start --debug",

.babelrc:

"env": {
  { ... }
  "test": {
    "presets": ["react", "es2015", "stage-1"],
    "plugins": ["transform-react-remove-prop-types", "rewire"]
  }
}

test.webpack.js

const context = require.context('./tests', true, /\.test\.js$/)

context.keys().forEach(context)

Лента

Tape — одна из самых простых библиотек для тестирования js. Если вы хотите узнать больше о Tape, в Интернете есть множество статей. Вот хороший:
https://medium.com/javascript-scene/why-i-use-tape-instead-of-mocha-so-should-you-6aa105d8eaf4#.ajekgomnc

Цитата из этой статьи гласит: «Вы можете думать о Tape как о чистом инструменте CLI, который принимает модульные тесты в качестве входных данных и создает TAP в качестве выходных». Я думаю, что это очень хорошо описывает, что такое плёнка. Лента работает с концепцией подтверждения мощности, поэтому вы можете добавлять собственные сообщения в свои тесты. Он отлично работает с es6, имеет отличное сообщество, а также вы можете расширять его утверждения. «tape-jsx-equals» отлично подходит для модульного тестирования React.

Фермент простой, но мощный.

shallow отлично подходит для модульного тестирования. Это очень быстро, а также дает вам возможность разделить ваш модульный тест и протестировать ваши компоненты изолированно. Идея состоит в том, что мелкий будет отображать только первый уровень компонента, заменяя другие компоненты, которые вы вызываете внутри его метода рендеринга, заглушками, и он даст вам объект со свойствами, такими как props, find('css selector here'), etc… 'mount' может дать вам полное монтирование вашего компонента и лучше для интеграционного тестирования. Если вы хотите узнать больше о ферменте, в Интернете достаточно информации. Вот пара ссылок, первая из документации энзима, а вторая — статья разработчика, который сам написал энзим:
http://airbnb.io/enzyme/docs/api/

https://medium.com/airbnb-engineering/enzyme-javascript-testing-utilities-for-react-a417e5e5090f#.fxa1wu4ni

Babel Rewire — это круто и мощно

«babel-plugin-rewire» дает вам возможность обрабатывать ваш импорт как «внедрение зависимостей». Все мы знаем, что функцию, которая имеет внутреннюю зависимость или вызывает другую функцию, не являющуюся обратным вызовом, трудно тестировать внутри. Это относится и к вашим компонентам. Возможно, они вызывают внешнюю функцию внутри, что не является действием. Но если эта функция импортирована и когда у нас есть babel-rewire, теперь вы можете издеваться над этой функцией, заменять ее пользовательской функцией или шпионить за ней. Вы также можете издеваться над своими импортированными действиями, константами, данными, селекторами или всем, что вам нужно для конкретного случая.

Хватит теории, давайте практиковаться

Давайте создадим новое хранилище для каждого теста, чтобы между ними не было общего состояния. Вы можете использовать функцию configureStore() напрямую, чтобы передать хранилище смонтированному компоненту, или, если у вас есть что-то более сложное, вы можете обернуть эту функцию в другую функцию, например корневой компонент, и экспортировать ее. По этой причине вы должны сделать свой корневой компонент функцией, например, createRoot(), которая ожидает сохранения. Вот пример этого

приложение/js/магазин/createStore.js

/* eslint-env browser */
import createSagaMiddleware from 'redux-saga'
import { applyMiddleware, compose, createStore } from 'redux'
import { routerMiddleware } from 'react-router-redux'
import { browserHistory } from 'react-router'
import rootReducer from '../reducers'
import rootSaga from '../saga'

export default function configureStore() {
    let middlewares = []
    let enhancers = []

    const sagaMiddleware = createSagaMiddleware()

    middlewares.push(sagaMiddleware, routerMiddleware(browserHistory))
    enhancers.push(applyMiddleware(...middlewares))

    const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
    const storeEnhancers = composeEnhancers(...enhancers)
    const store = createStore(rootReducer, storeEnhancers)

    if (module.hot) {
        module.hot.accept('../reducers/', () => {
            const nextRootReducer = require('../reducers/index').default

            store.replaceReducer(nextRootReducer)
        })
    }

    sagaMiddleware.run(rootSaga)

    return store
}

приложение/js/компоненты/Root.js

import React, { Component } from 'react'
import { render } from 'react-dom'
import { Router, Route, IndexRedirect, browserHistory } from 'react-router'
import { Provider } from 'react-redux'
import { syncHistoryWithStore } from 'react-router-redux'
import configureStore from '../store/createStore'
import { App, TodosContainer, HomePageContainer } from '../containers'
import getLink from '../utils/getLink'

export const configureStoreWithBrowserHistory = () => {
    const store = configureStore()

    syncHistoryWithStore(browserHistory, store)

    return store
}

const defaultStore = configureStoreWithBrowserHistory()

export const createRoot = (store = defaultStore, name = 'Root') => {
    class Root extends Component {
        render() {
            return (
                <Provider store={store}>
                    <Router history={browserHistory}>
                        <Route path={getLink('')} component={App}>
                            <IndexRedirect to={getLink('home')} />
                            <Route path={getLink('home')} component={HomePageContainer} />
                            <Route path={getLink('todos')} component={TodosContainer} />
                            <Route path='*' component={HomePageContainer} />
                        </Route>
                    </Router>
                </Provider>
            )
        }
    }

    Root.displayName = name

    return Root
}

приложение/js/index.js

/* eslint-env browser */
import React from 'react'
import { render } from 'react-dom'
import { createRoot } from './components/Root'
import '../css/index.css'

const RootComponent = createRoot()

const run = () => {
    render(
        <RootComponent />,
        document.getElementById('app')
    )
}

window.addEventListener('DOMContentLoaded', run, false)

тесты/sample.test.js

import 'babel-polyfill'
import test from 'tape'
import React from 'react'
import { push } from 'react-router-redux'
import { mount } from 'enzyme'
import getLink from '../app/js/utils/getLink'
import { configureStoreWithBrowserHistory, createRoot } from '../app/js/components/Root'
test('Test: sample test', t => {
    // This is how we create new instance of the store and the Root for every component 
    const store = configureStoreWithBrowserHistory()
    const Root = createRoot(store)
    const wrapper = mount(<Root />)
    
    // Set browser history to be /todos. This will make your Root component render TodosContainer
    store.dispatch(push(getLink('todos')))

    // Find a button on which you want to trigger an action
    const button = wrapper.find('.fa-download')

    // Test if a service is called as a process of you pressing the button.
    // You can include other tests that test if correct action is called or UI is behaving correctly.
    todosSaga.__Rewire__(SAMPLE_SERVICE, options => {
        // You can include check if service is called with correct options
        t.pass(
            `Clicking on Exception Summary icon should call the correct service.
             "${SAMPLE_SERVICE}" is called.`
        )

        // Don't forget to reset the dependency so this doesn't affect other tests
        todosSaga.__ResetDependency__(SAMPLE_SERVICE)
        // unmount the component
        wrapper.unmount()
        // end the test
        t.end()

        // return something if you dont want your program to break
        return {}
    })
    
    button.simulate('click')
})

Вы можете посмотреть весь образец проекта и другие тесты здесь:
https://github.com/danielpetrov/testing-react-redux-with-karma-tape-enzyme-babel-rewire

Если у вас, как у читателя, есть вопросы, задавайте их в комментариях.