使用redux thunk

时间:2016-10-20 22:32:21

标签: 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'
      })
  }
}

我无法将测试编写到调度中,调度要么被调用,要么不被调用(取决于服务器响应),或者我可以让调用操作并检查存储中的值是否正确更新。

我使用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()
})

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

我在测试中做错了什么?

2 个答案:

答案 0 :(得分:19)

在Redux中测试异步Thunk操作

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

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

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

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

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

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

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

动作与动作创作者

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

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

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

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

它只是一个返回对象的函数。 我们知道此对象是redux操作,因为其中一个属性称为类型。

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

const walkDogAction = addTodo('walk the dog')

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

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

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

store.dispatch(walkDogAction)

大。

我们已经派出了这个物品,它将直接进入减速器并用新的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);
    }

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))
    })
  }
}

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

创建我们的Mock Redux商店

在测试文件中,从redux-mock-store库导入配置存储功能,以创建我们的虚假商店。

import configureStore from 'redux-mock-store';

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

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

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

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

然后,我们进行测试断言,以比较调度到模拟商店的实际操作与我们预期的操作。

测试我们的thunk动作创建者在Mocha中返回的承诺

因此,在测试结束时,我们将thunk动作创建者发送到模拟商店。我们一定不要忘记返回这个调度调用,这样当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()
  })
})

请记住,因为我们的Redux thunk动作创建者本身并不是一个动作,只是为了派遣进一步的动作而存在。

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

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

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

始终确保在为行动创建者使用承诺时返回动作创建者返回的函数内的承诺。

    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子句中履行或拒绝承诺后作出断言。但是。然后我们才开始工作,因为我们只能打电话给承诺。由于我们忘记返回动作创建者中的最后一个嵌套函数,它返回一个承诺,然后我们将调用.then on undefined。它未定义的原因是因为函数范围内没有return语句。

因此,在调用return promises时,总是返回动作创建者中的函数。

答案 1 :(得分:1)

您可能希望考虑使用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'
  })

等等 - 你会看到.then实际上需要两个独立的功能,一个用于成功,一个用于错误。