Spec-Zone.ru › Redux 3

Написание тестов

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

Настройка

Мы рекомендуем Jest в качестве движка для тестирования. Обратите внимание, что он работает в среде Node, поэтому у вас не будет доступа к DOM.

npm install --save-dev jest

Для использования его вместе с Babel вам нужно установить babel-jest:

npm install --save-dev babel-jest

и настроить его для использования функций ES2015 в .babelrc:

{
  "presets": ["es2015"]
}

Затем добавьте это в scripts в вашем package.json:

{
  ...
  "scripts": {
    ...
    "test": "jest",
    "test:watch": "npm test -- --watch"
  },
  ...
}

и запустите npm test для запуска один раз или npm run test:watch для тестирования при каждом изменении файла.

Создатели действий

В Redux создатели действий — это функции, которые возвращают простые объекты. При тестировании создателей действий мы хотим проверить, был ли вызван правильный создатель действия, а также возвращалось ли правильное действие.

Пример

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}

можно протестировать следующим образом:

import * as actions from '../../actions/TodoActions'
import * as types from '../../constants/ActionTypes'

describe('actions', () => {
  it('should create an action to add a todo', () => {
    const text = 'Finish docs'
    const expectedAction = {
      type: types.ADD_TODO,
      text
    }
    expect(actions.addTodo(text)).toEqual(expectedAction)
  })
})

Асинхронные создатели действий

Для асинхронных создателей действий с использованием Redux Thunk или другого middleware лучше всего полностью смоделировать хранилище Redux для тестов. Вы можете применить middleware к моделируемому хранилищу с помощью redux-mock-store. Вы также можете использовать nock для моделирования HTTP-запросов.

Пример

import fetch from 'isomorphic-fetch'

function fetchTodosRequest() {
  return {
    type: FETCH_TODOS_REQUEST
  }
}

function fetchTodosSuccess(body) {
  return {
    type: FETCH_TODOS_SUCCESS,
    body
  }
}

function fetchTodosFailure(ex) {
  return {
    type: FETCH_TODOS_FAILURE,
    ex
  }
}

export function fetchTodos() {
  return dispatch => {
    dispatch(fetchTodosRequest())
    return fetch('http://example.com/todos')
      .then(res => res.json())
      .then(json => dispatch(fetchTodosSuccess(json.body)))
      .catch(ex => dispatch(fetchTodosFailure(ex)))
  }
}

можно протестировать следующим образом:

import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import * as actions from '../../actions/TodoActions'
import * as types from '../../constants/ActionTypes'
import nock from 'nock'
import expect from 'expect' // You can use any testing library

const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)

describe('async actions', () => {
  afterEach(() => {
    nock.cleanAll()
  })

  it('creates FETCH_TODOS_SUCCESS when fetching todos has been done', () => {
    nock('http://example.com/')
      .get('/todos')
      .reply(200, { body: { todos: ['do something'] } })

    const expectedActions = [
      { type: types.FETCH_TODOS_REQUEST },
      { type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something'] } }
    ]
    const store = mockStore({ todos: [] })

    return store.dispatch(actions.fetchTodos()).then(() => {
      // return of async actions
      expect(store.getActions()).toEqual(expectedActions)
    })
  })
})

Редьюсеры

Редьюсер должен возвращать новое состояние после применения действия к предыдущему состоянию, и это поведение тестируется ниже.

Пример

import { ADD_TODO } from '../constants/ActionTypes'

const initialState = [
  {
    text: 'Use Redux',
    completed: false,
    id: 0
  }
]

export default function todos(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        {
          id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
          completed: false,
          text: action.text
        },
        ...state
      ]

    default:
      return state
  }
}

можно протестировать следующим образом:

import reducer from '../../reducers/todos'
import * as types from '../../constants/ActionTypes'

describe('todos reducer', () => {
  it('should return the initial state', () => {
    expect(reducer(undefined, {})).toEqual([
      {
        text: 'Use Redux',
        completed: false,
        id: 0
      }
    ])
  })

  it('should handle ADD_TODO', () => {
    expect(
      reducer([], {
        type: types.ADD_TODO,
        text: 'Run the tests'
      })
    ).toEqual([
      {
        text: 'Run the tests',
        completed: false,
        id: 0
      }
    ])

    expect(
      reducer(
        [
          {
            text: 'Use Redux',
            completed: false,
            id: 0
          }
        ],
        {
          type: types.ADD_TODO,
          text: 'Run the tests'
        }
      )
    ).toEqual([
      {
        text: 'Run the tests',
        completed: false,
        id: 1
      },
      {
        text: 'Use Redux',
        completed: false,
        id: 0
      }
    ])
  })
})

Компоненты

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

Сначала мы установим Enzyme. Enzyme использует React Test Utilities внизу, но является более удобным, читабельным и мощным.

npm install --save-dev enzyme

Для тестирования компонентов мы создаём setup() помощник, который передаёт подставленные колбэки в качестве свойств и рендерит компонент с использованием поверхностного рендеринга. Это позволяет отдельным тестам проверять, вызывались ли колбэки в ожидаемом порядке.

Пример

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import TodoTextInput from './TodoTextInput'

class Header extends Component {
  handleSave(text) {
    if (text.length !== 0) {
      this.props.addTodo(text)
    }
  }

  render() {
    return (
      <header className="header">
        <h1>todos</h1>
        <TodoTextInput
          newTodo={true}
          onSave={this.handleSave.bind(this)}
          placeholder="What needs to be done?"
        />
      </header>
    )
  }
}

Header.propTypes = {
  addTodo: PropTypes.func.isRequired
}

export default Header

можно протестировать следующим образом:

import React from 'react'
import { mount } from 'enzyme'
import Header from '../../components/Header'

function setup() {
  const props = {
    addTodo: jest.fn()
  }

  const enzymeWrapper = mount(<Header {...props} />)

  return {
    props,
    enzymeWrapper
  }
}

describe('components', () => {
  describe('Header', () => {
    it('should render self and subcomponents', () => {
      const { enzymeWrapper } = setup()

      expect(enzymeWrapper.find('header').hasClass('header')).toBe(true)

      expect(enzymeWrapper.find('h1').text()).toBe('todos')

      const todoInputProps = enzymeWrapper.find('TodoTextInput').props()
      expect(todoInputProps.newTodo).toBe(true)
      expect(todoInputProps.placeholder).toEqual('What needs to be done?')
    })

    it('should call addTodo if length of text is greater than 0', () => {
      const { enzymeWrapper, props } = setup()
      const input = enzymeWrapper.find('TodoTextInput')
      input.props().onSave('')
      expect(props.addTodo.mock.calls.length).toBe(0)
      input.props().onSave('Use Redux')
      expect(props.addTodo.mock.calls.length).toBe(1)
    })
  })
})

Подключенные компоненты

Если вы используете библиотеку, такую как React Redux, вы можете использовать высокоуровневые компоненты, такие как connect(). Это позволяет вам вставлять состояние Redux в обычный компонент React.

Рассмотрим следующий App компонент:

import { connect } from 'react-redux'

class App extends Component { /* ... */ }

export default connect(mapStateToProps)(App)

В unit-тесте вы обычно импортируете App компонент так:

import App from './App'

Однако, при импорте вы фактически держите обернутый компонент, возвращаемый connect(), а не сам App компонент. Если вы хотите протестировать его взаимодействие с Redux, это хорошая новость: вы можете обернуть его в <Provider> с хранилищем, созданным специально для этого unit-теста. Но иногда вы хотите проверить только отображение компонента без хранилища Redux.

Для того чтобы протестировать сам компонент App без необходимости обработки декоратора, мы рекомендуем экспортировать также и неотдекорированный компонент:

import { connect } from 'react-redux'

// Use named export for unconnected component (for tests)
export class App extends Component { /* ... */ }

// Use default export for the connected component (for app)
export default connect(mapStateToProps)(App)

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

// Note the curly braces: grab the named export instead of default export
import { App } from './App'

И если вам нужно и то, и другое:

import ConnectedApp, { App } from './App'

В самом приложении вы по-прежнему импортируете его обычным способом:

import App from './App'

Вы будете использовать только именованный экспорт для тестов.

Примечание по смешиванию ES6 модулей и CommonJS

Если вы используете ES6 в своём коде приложения, но пишете тесты в ES5, вы должны знать, что Babel обрабатывает взаимозаменяемое использование ES6 import и CommonJS require с помощью своей возможности interop для запуска двух форматов модулей бок о бок, но поведение немного отличается. Если вы добавите второй экспорт рядом с вашим экспортом по умолчанию, вы больше не сможете импортировать экспорт по умолчанию, используя require('./App'). Вместо этого вам нужно использовать require('./App').default.

Middleware

Функции middleware оборачивают поведение вызовов dispatch в Redux, поэтому для тестирования этого изменённого поведения нам нужно смоделировать поведение вызова dispatch.

Пример

Сначала нам понадобится функция middleware. Она похожа на реальную redux-thunk.

const thunk = ({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState)
  }

  return next(action)
}

Нам нужно создать фейковые getState, dispatch, и next функции. Мы используем jest.fn() для создания заглушек, но в других тестовых фреймворках вы, скорее всего, будете использовать sinon.

Функция invoke запускает наш middleware таким же образом, как и Redux.

const create = () => {
  const store = {
    getState: jest.fn(() => ({})),
    dispatch: jest.fn(),
  };
  const next = jest.fn()

  const invoke = (action) => thunk(store)(next)(action)

  return {store, next, invoke}
};

Мы проверяем, что наш middleware вызывает getState, dispatch, и next функции в нужное время.

it(`passes through non-function action`, () => {
  const { next, invoke } = create()
  const action = {type: 'TEST'}
  invoke(action)
  expect(next).toHaveBeenCalledWith(action)
})

it('calls the function', () => {
  const { invoke } = create()
  const fn = jest.fn()
  invoke(fn)
  expect(fn).toHaveBeenCalled()
});

it('passes dispatch and getState', () => {
  const { store, invoke } = create()
  invoke((dispatch, getState) => {
    dispatch('TEST DISPATCH')
    getState();
  })
  expect(store.dispatch).toHaveBeenCalledWith('TEST DISPATCH')
  expect(store.getState).toHaveBeenCalled()
});

В некоторых случаях вам необходимо изменить функцию create для использования различных имитаций getState и next.

Глоссарий

  • Enzyme: Enzyme — это JavaScript-утилита для тестирования React, которая упрощает проверку, манипулирование и навигацию по выводу компонентов React.

  • React Test Utils: Утилиты для тестирования React. Используются Enzyme.

  • Поверхностный рендеринг: Поверхностный рендеринг позволяет вам создать компонент и получить результат его render метода только на одном уровне вглубь, а не рендерить компоненты рекурсивно в DOM. Поверхностный рендеринг полезен для unit-тестов, где вы тестируете только конкретный компонент и, что важно, не его потомков. Это также означает, что изменение дочернего компонента не повлияет на тесты родительского компонента. Тестирование компонента и всех его потомков можно выполнить с помощью метода Enzyme mount(), то есть полного рендеринга DOM.

© 2015–2017 Dan Abramov
Licensed under the MIT License.
http://redux.js.org/docs/recipes/WritingTests.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API