使用redux thunk测试异步操作

Huy*_*Huy 7 reactjs redux redux-thunk redux-mock-store

我正在尝试测试具有异步调用的操作.我使用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'
      })
  }
}
Run Code Online (Sandbox Code Playgroud)

我在编写测试时遇到问题,调试要么调用调用,要么不调用(取决于服务器响应),或者我可以让调用操作并检查存储中的值是否正确更新.

我使用fetch-mock来模拟web的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()
})
Run Code Online (Sandbox Code Playgroud)

在进一步研究之后,我认为fetch-mock有问题,要么不解析promise,要么执行then语句,要么完全删除fetch.当我向两个语句添加一个console.log时,没有任何执行.

我在测试中做错了什么?

the*_*ode 25

在Redux中测试异步Thunk动作

您没有在任何测试中调用setSubscribed redux-thunk动作创建器.相反,您正在定义相同类型的新操作,并尝试在测试中分发该操作.

在两个测试中,将同步调度以下操作.

  const subscribed = { type: 'SET_SUBSCRIBED', subscribed: true }
Run Code Online (Sandbox Code Playgroud)

在此操作中,不会向任何API发出请求.

我们希望能够从外部API获取,然后在成功或失败时调度操作.

由于我们将来在某个时间段调度动作,我们需要使用你的setSubscribed thunk动作创建器.

在简要解释了redux-thunk如何工作后,我将解释如何测试这个thunk动作创建者.

动作与动作创作者

也许值得解释一下,动作创建者是一个在被调用时返回一个动作对象的函数.

术语动作指的是物体本身.对于此操作对象,唯一的必需属性是type应该是字符串.

例如,这里是一个动作创建者.

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}
Run Code Online (Sandbox Code Playgroud)

它只是一个返回对象的函数.我们知道这个对象是一个redux动作,因为它的一个属性叫做type.

它创建toDos以按需添加.让我们做一个新的待办事项来提醒我们狗走路.

const walkDogAction = addTodo('walk the dog')

console.log(walkDogAction)
* 
* { type: 'ADD_TO_DO, text: 'walk the dog' }
*
Run Code Online (Sandbox Code Playgroud)

此时我们有一个由我们的动作创建者生成的动作对象.

现在,如果我们想要将此操作发送到我们的reducer以更新我们的存储,那么我们使用action对象作为参数调用dispatch.

store.dispatch(walkDogAction)
Run Code Online (Sandbox Code Playgroud)

大.

我们已经派出了这个物品,它将直接进入减速器并用新的todo更新我们的商店,提醒我们遛狗.

我们如何制定更复杂的行动?如果我希望我的动作创建者执行依赖于异步操作的操作,该怎么办?

同步与异步Redux操作

异步(异步)和同步(同步)是什么意思?

当您同步执行某些操作时,请等待它完成, 然后再继续执行其他任务.当您异步执行某些操作时,可以在完成之前继续执行另一个任务.

好吧,如果我想让我的狗去取东西?在这种情况下,我关心三件事

  • 当我让他去取物品的时候
  • 他成功取得了什么?
  • 他没有取得这个物品吗?(即没有棍子回到我身边,在给定的时间后没有回到我身边)

可能很难想象这可以用一个单独的对象来表示,就像我们的addtodo动作一样,它只是由一种类型和一段文本组成.

而不是作为对象的动作,它需要是一个功能.为什么一个功能?函数可用于分派进一步的操作.

我们将fetch的重要总体动作分为三个较小的同步动作.我们的主要获取操作创建者是异步的.请记住,此主要操作创建者本身不是一个操作,它仅用于发送进一步的操作.

Thunk Action的创建者如何运作?

本质上,thunk动作创建者是返回函数而不是对象的动作创建者.通过将redux-thunk添加到我们的中间件存储中,这些特殊操作将可以访问商店的dispatch和getState方法.

Here is the code inside Redux thunk that does this:

    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
Run Code Online (Sandbox Code Playgroud)

setSubscribed函数是一个thunk动作创建者,因为它遵循返回一个以dispatch作为参数的函数的签名.

好的,这就是为什么我们的thunk action creator返回一个函数.因为这个函数将被中间件调用并让我们访问调度和获取状态,这意味着我们可以在以后调度进一步的操作.

使用动作建模异步操作

让我们写下我们的行动.我们的redux thunk动作创建器负责异步调度表示异步动作生命周期的其他三个动作,在这种情况下是一个http请求.请记住,此模型适用于任何异步操作,因为必须有开头和结果标记成功或某些错误(失败)

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))
    })
  }
}
Run Code Online (Sandbox Code Playgroud)

你可能知道最后一个动作是redux thunk动作创建者.我们知道这是因为它是唯一返回函数的动作.

创建我们的Mock Redux商店

在测试文件中,从redux-mock-store库导入configure store函数以创建我们的假存储.

import configureStore from 'redux-mock-store';
Run Code Online (Sandbox Code Playgroud)

此模拟存储将在数组中调度的操作将在您的测试中使用.

由于我们正在测试一个thunk动作创建者,我们的模拟商店需要在我们的测试中配置redux-thunk中间件,否则我们的商店将无法处理thunk动作创建者.或者换句话说,我们将无法调度函数而不是对象.

const middlewares = [ReduxThunk];
const mockStore = configureStore(middlewares);
Run Code Online (Sandbox Code Playgroud)

Out mock store有一个store.getActions方法,当调用它时会给我们一个包含所有以前调度的动作的数组.

然后,我们使用测试断言来比较调度到模拟存储的实际操作与我们预期的操作.

测试我们的摩卡动作创建者返回的承诺

因此,在测试结束时,我们将thunk action creator发送到模拟商店.我们不能忘记返回此调度调用,以便在thunk动作创建者返回的promise被解析时,断言将在.then块中运行.

工作测试

如果您使用上述操作将此测试文件复制到您的应用程序中,请确保安装所有软件包并正确导入下面的测试文件中的操作,然后您将有一个测试redux thunk action creators的工作示例,以确保他们调度正确的行动.

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()
  })
})
Run Code Online (Sandbox Code Playgroud)

请记住,因为我们的Redux thunk动作创建者本身不是一个动作,只存在于发送进一步的动作.

我们对thunk动作创建者的大部分测试都将专注于对在特定条件下调度的其他操作进行断言.

这些特定条件是异步操作的状态,其可以是超时的http请求或表示成功的200状态.

测试Redux Thunk时常见的问题 - 在Action Creators中没有返回承诺

始终确保在为动作创建者使用promises时,您在动作创建者返回的函数内返回 promise.

    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
            })
          }
     }
Run Code Online (Sandbox Code Playgroud)

因此,如果没有返回最后一个嵌套函数,那么当我们尝试异步调用该函数时,我们将得到错误:

TypeError: Cannot read property 'then' of undefined - store.dispatch - returns undefined
Run Code Online (Sandbox Code Playgroud)

那是因为我们试图在.then条款中履行或拒绝承诺之后做出断言.但是.然后才行,因为我们只能在承诺上打电话.因为我们忘记返回动作创建器中的最后一个嵌套函数,它返回一个promise,然后我们将调用.then on undefined.它未定义的原因是因为函数范围内没有return语句.

所以总是在动作创建器中返回函数,当调用return函数时.


Chr*_*ins 0

您可能希望考虑使用 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'
  })
Run Code Online (Sandbox Code Playgroud)

依此类推 - 您会看到 .then 实际上需要两个单独的函数,一个用于成功,一个用于错误。