Тестирование асинхронных действий с помощью redux thunk

#reactjs #redux #redux-thunk #redux-mock-store

#reactjs #redux #функция redux-thunk #redux-макет хранилища

Вопрос:

Я пытаюсь протестировать свое действие, которое имеет асинхронный вызов. Я использую Thunk в качестве промежуточного программного обеспечения. В приведенном ниже действии я отправляю и обновляю хранилище только в том случае, если сервер возвращает OK ответ.

 export const SET_SUBSCRIBED = 'SET_SUBSCRIBED'

export const setSubscribed = (subscribed) => {
  return function(dispatch) {
    var url = 'https://api.github.com/users/1/repos';

    return fetch(url, {method: 'GET'})
      .then(function(result) {
        if (result.status === 200) {
          dispatch({
            type: SET_SUBSCRIBED,
            subscribed: subscribed
          })
          return 'result'
        }
        return 'failed' //todo
      }, function(error) {
        return 'error'
      })
  }
}
  

У меня возникли проблемы с написанием тестов либо для тестов, отправка которых либо вызывается, либо нет (в зависимости от ответа сервера), либо я мог бы просто разрешить вызов действия и проверить, что значение в хранилище обновлено правильно.

Я использую fetch-mock для моделирования веб-реализации fetch(). Однако, похоже, что блок моего кода в then не выполняется. Я также пытался использовать приведенный здесь пример, но безуспешно — http://redux.js.org/docs/recipes/WritingTests.html

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

//passing test
it('returns SET_SUBSCRIBED type and subscribed true', () => {
  fetchMock.get('https://api.github.com/users/1/repos', { status: 200 })

  const subscribed = { type: 'SET_SUBSCRIBED', subscribed: true }
  const store = mockStore({})

  store.dispatch(subscribed)

  const actions = store.getActions()

  expect(actions).toEqual([subscribed])
  fetchMock.restore()
})

//failing test
it('does nothing', () => {
  fetchMock.get('https://api.github.com/users/1/repos', { status: 400 })

  const subscribed = { type: 'SET_SUBSCRIBED', subscribed: true }
  const store = mockStore({})

  store.dispatch(subscribed)

  const actions = store.getActions()

  expect(actions).toEqual([])
  fetchMock.restore()
})
  

Изучив это еще немного, я полагаю, что с fetch-mock что-то не так, либо он не разрешает promise, чтобы выполнялись инструкции then, либо он полностью отключает выборку. Когда я добавляю консоль.войдите в оба оператора then, ничего не выполняется.

Что я делаю неправильно в своих тестах?

Ответ №1:

Тестирование асинхронных действий Thunk в Redux

Вы не вызываете setSubscribed redux-thunk action creator ни в одном из своих тестов. Вместо этого вы определяете новое действие того же типа и пытаетесь отправить его в свой тест.

В обоих ваших тестах следующее действие отправляется синхронно.

   const subscribed = { type: 'SET_SUBSCRIBED', subscribed: true }
  

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

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

Поскольку мы отправляем действие в какой-то момент в будущем, нам нужно использовать ваш создатель действия thunk с установленной подпиской.

После краткого объяснения того, как работает redux-thunk, я объясню, как протестировать этот создатель действий thunk.

Действия против создателей действий

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

Термин действие относится к самому объекту. Для этого объекта action единственным обязательным свойством является type, который должен быть строкой.

Например, вот создатель действий.

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

Это просто функция, которая возвращает объект. Мы знаем, что этот объект является действием redux, потому что одно из его свойств называется type .

Он создает задачи для добавления по требованию. Давайте создадим новое задание, которое напомнит нам о выгуле собак.

 const walkDogAction = addTodo('walk the dog')

console.log(walkDogAction)
* 
* { type: 'ADD_TO_DO, text: 'walk the dog' }
*
  

На данный момент у нас есть объект action, который был сгенерирован нашим создателем action.

Теперь, если мы хотим отправить это действие нашим редукторам для обновления нашего хранилища, тогда мы вызываем dispatch с объектом action в качестве аргумента.

 store.dispatch(walkDogAction)
  

Great.

We have dispatched the object and it will go straight to the reducers and update our store with the new todo reminding us to walk the dog.

How do we make more complex actions? What if I want my action creator to do something that relies on an asynchronous operation.

Синхронные и асинхронные действия Redux

Что мы подразумеваем под асинхронностью (asynchronous) и синхронизацией (synchronous)?

Когда вы выполняете что-то синхронно, вы ждете его завершения, прежде чем перейти к другой задаче. Когда вы выполняете что-то асинхронно, вы можете перейти к другой задаче до ее завершения.

Хорошо, итак, если я хочу попросить свою собаку принести что-нибудь? В этом случае меня волнуют три вещи

  • когда я попросил его извлечь объект
  • он что-то успешно извлек?
  • ему не удалось извлечь объект? (т. е. вернулся ко мне без стика, вообще не возвращался ко мне через заданное количество времени)

Вероятно, трудно представить, как это может быть представлено одним объектом, таким как наше действие addtodo для выгула собаки, которое просто состояло из типа и фрагмента текста.

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

Мы разделили большое всеобъемлющее действие выборки на три меньших синхронных действия. Наш основной создатель действий выборки — асинхронный. Помните, что этот создатель основного действия сам по себе не является действием, он существует только для отправки дальнейших действий.

Как работает создатель действий Thunk?

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

 Here is the code inside Redux thunk that does this:

    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
  

Функция setSubscribed является создателем действия thunk, поскольку она следует сигнатуре возврата функции, которая принимает отправку в качестве аргумента.

Хорошо, вот почему наш создатель thunk action возвращает функцию. поскольку эта функция будет вызвана промежуточным программным обеспечением и предоставит нам доступ к dispatch и состоянию get, это означает, что мы сможем отправлять дальнейшие действия позже.

Моделирование асинхронных операций с помощью действий

Давайте напишем наши действия. наш создатель действия redux thunk отвечает за асинхронную отправку трех других действий, которые представляют жизненный цикл нашего асинхронного действия, которое в данном случае является http-запросом. Помните, что эта модель применима к любому асинхронному действию, поскольку обязательно есть начало и результат, которые отмечают успех или некоторую ошибку (failure)

actions.js

 export function fetchSomethingRequest () {
  return {
    type: 'FETCH_SOMETHING_REQUEST'
  }
}

export function fetchSomethingSuccess (body) {
  return {
    type: 'FETCH_SOMETHING_SUCCESS',
    body: body
  }
}

export function fetchSomethingFailure (err) {
  return {
    type: 'FETCH_SOMETHING_FAILURE',
    err
  }
}

export function fetchSomething () {
  return function (dispatch) {
    dispatch(fetchSomethingRequest())
    return fetchSomething('http://example.com/').then(function (response) {
      if (response.status !== 200) {
        throw new Error('Bad response from server')
      } else {
        dispatch(fetchSomethingSuccess(response))
      }
    }).catch(function (reason) {
      dispatch(fetchSomethingFailure(reason))
    })
  }
}
  

Как вы, вероятно, знаете, последнее действие — это создатель действия redux thunk. Мы знаем это, потому что это единственное действие, которое возвращает функцию.

Создаем наш макет хранилища Redux

В тестовый файл импортируйте функцию configure store из библиотеки redux-mock-store, чтобы создать наше поддельное хранилище.

 import configureStore from 'redux-mock-store';
  

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

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

 const middlewares = [ReduxThunk];
const mockStore = configureStore(middlewares);
  

В нашем макетном хранилище есть метод store.getActions, который при вызове выдает нам массив всех ранее отправленных действий.

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

Тестирование обещания, возвращенного нашим создателем thunk action в Mocha

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

Рабочие тесты

Если вы скопируете этот тестовый файл в свое приложение с описанными выше действиями, убедившись, что правильно установили все пакеты и импортировали действия из приведенного ниже тестового файла, то у вас будет рабочий пример тестирования создателей redux thunk action, чтобы убедиться, что они отправляют правильные действия.

 import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import fetchMock from 'fetch-mock'  // You can use any http mocking library
import expect from 'expect' // You can use any testing library

import { fetchSomething } from './actions.js'

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

describe('Test thunk action creator', () => {
  it('expected actions should be dispatched on successful request', () => {
    const store = mockStore({})
    const expectedActions = [ 
        'FETCH_SOMETHING_REQUEST', 
        'FETCH_SOMETHING_SUCCESS'
    ]

 // Mock the fetch() global to always return the same value for GET
 // requests to all URLs.
 fetchMock.get('*', { response: 200 })

    return store.dispatch(fetchSomething())
      .then(() => {
        const actualActions = store.getActions().map(action => action.type)
        expect(actualActions).toEqual(expectedActions)
     })

    fetchMock.restore()
  })

  it('expected actions should be dispatched on failed request', () => {
    const store = mockStore({})
    const expectedActions = [ 
        'FETCH_SOMETHING_REQUEST', 
        'FETCH_SOMETHING_FAILURE'
    ]
 // Mock the fetch() global to always return the same value for GET
 // requests to all URLs.
 fetchMock.get('*', { response: 404 })

    return store.dispatch(fetchSomething())
      .then(() => {
        const actualActions = store.getActions().map(action => action.type)
        expect(actualActions).toEqual(expectedActions)
     })

    fetchMock.restore()
  })
})
  

Помните, поскольку наш создатель действия Redux thunk не является самим действием и существует только для отправки дальнейших действий.

Большая часть нашего тестирования создателей thunk-действий будет сосредоточена на создании утверждений о том, какие именно дополнительные действия отправляются при определенных условиях.

Этими конкретными условиями являются состояние асинхронной операции, которое может быть http-запросом с истекшим временем ожидания или статусом 200, представляющим успех.

Распространенная ошибка при тестировании функций Redux Thunksне возвращаются обещания в Action Creators

Всегда убедитесь, что при использовании promises для создателей действий вы возвращаете обещание внутри функции, возвращаемой создателем действия.

     export function thunkActionCreator () {
          return function thatIsCalledByreduxThunkMiddleware() {

            // Ensure the function below is returned so that 
            // the promise returned is thenable in our tests
            return function returnsPromise()
               .then(function (fulfilledResult) {
                // do something here
            })
          }
     }
  

Итак, если эта последняя вложенная функция не возвращается, то при попытке вызвать функцию асинхронно мы получим ошибку:

 TypeError: Cannot read property 'then' of undefined - store.dispatch - returns undefined
  

Это потому, что мы пытаемся сделать утверждение после того, как обещание выполнено или отклонено в .затем предложение. Однако .then не будет работать, потому что мы можем только вызывать .затем по обещанию. Поскольку мы забыли вернуть последнюю вложенную функцию в action creator, которая возвращает обещание, тогда мы будем вызывать .затем на undefined. Причина, по которой это не определено, заключается в том, что в области действия функции нет оператора return.

Поэтому всегда возвращайте функции в action creators, которые при вызове возвращают обещания.

Ответ №2:

Возможно, вы захотите рассмотреть возможность использования DevTools — это позволит вам четко видеть отправляемое действие, а также ваше состояние до / после вызова. Если отправка никогда не происходит, то, возможно, выборка не возвращает ошибку типа 200.

Тогда обещание должно быть:

 return fetch(url, {
    method: 'GET'
  })
  .then(function(result) {
    if (result.status === 200) {
      dispatch({
        type: SET_SUBSCRIBED,
        subscribed: subscribed
      })
      return 'result'
    }
    return 'failed' //todo
  }, function(error) {
    return 'error'
  })
  

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

Комментарии:

1. Однако это происходит во время теста. Код работает нормально — я просто хочу знать, как я могу его протестировать.

2. Ах, хорошо, извините — я полагаю, это потому, что в вашем. тогда у вас есть только одна функция — она будет вызываться только в случае успеха (200 ответов); вам нужно будет добавить после нее другую функцию для обработки состояния ошибки. Я обновлю свой ответ, чтобы отразить это.

3. Я пробовал это, но это не проблема с реальным кодом. Это проблема с тестом. Я не уверен, где это, но я не думаю, что я должным образом тестирую свое асинхронное действие