使用 redux thunk 测试异步操作

Posted

技术标签:

【中文标题】使用 redux thunk 测试异步操作【英文标题】:Testing async actions with redux thunk 【发布时间】:2017-03-03 02:39:44 【问题描述】:

我正在尝试测试具有异步调用的操作。我使用 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 语句执行,要么完全取消了 fetch。当我将 console.log 添加到两个 then 语句时,什么都不会执行。

我在测试中做错了什么?

【问题讨论】:

【参考方案1】:

在 Redux 中测试异步 Thunk 操作

您没有在任何测试中调用 setSubscribed redux-thunk action creator。相反,您正在定义一个相同类型的新操作并尝试在您的测试中调度它。

在您的两个测试中,以下操作是同步分派的。

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

在此操作中,没有向任何 API 发出请求。

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

由于我们将在未来的某个时间分派操作,因此我们需要使用您的 setSubscribed thunk 操作创建器。

在简要解释 redux-thunk 的工作原理后,我将解释如何测试这个 thunk action creator。

Action 与 Action Creator

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

action 一词指的是对象本身。对于这个动作对象,唯一的强制属性是类型,它应该是一个字符串。

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

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 以更新我们的存储,那么我们调用 dispatch 并将操作对象作为参数。

store.dispatch(walkDogAction)

太好了。

我们已经发送了这个对象,它会直接进入减速器,并用新的 todo 更新我们的商店,提醒我们遛狗。

我们如何做出更复杂的动作?如果我希望我的动作创建者做一些依赖于异步操作的事情。

同步与异步 Redux 操作

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

当你同步执行某事时,你等待它完成 在继续执行另一项任务之前。当你执行某事时 异步,您可以在另一个任务完成之前继续它

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

当我让他取一个对象时 他成功获取了一些东西吗? 他是否未能获取对象? (即没有棍子回到我身边,在给定的时间后根本没有回到我身边

可能很难想象这如何用单个对象来表示,例如我们用于遛狗的 addtodo 动作,它只包含一个类型和一段文本。

动作应该是一个函数,而不是一个对象。为什么是函数?函数可用于调度进一步的操作。

我们将 fetch 的大型总体操作拆分为三个较小的同步操作。我们的主要获取操作创建者是异步的。请记住,这个主要动作创建者并不是动作本身,它只是为了调度进一步的动作而存在。

Think 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 动作创建器返回一个函数的原因。因为这个函数将被中间件调用,让我们可以访问调度和获取状态,这意味着我们可以在以后调度进一步的操作。

使用动作建模异步操作

让我们编写我们的操作。我们的 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 库中导入 configure store 函数来创建我们的假存储。

import configureStore from 'redux-mock-store';

这个模拟存储将在你的测试中使用的数组中调度的动作。

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

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

Out mock store 有一个 store.getActions 方法,当被调用时,它会为我们提供一个包含所有先前调度的操作的数组。

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

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

因此,在测试结束时,我们将 thunk 动作创建者发送到模拟存储。我们不能忘记返回这个调度调用,这样当 thunk 动作创建者返回的承诺被解决时,断言将在 .then 块中运行。

工作测试

如果您使用上述操作将此测试文件复制到您的应用程序中,请确保安装所有包并正确导入以下测试文件中的操作,那么您将有一个测试 redux thunk 操作创建者的工作示例,以确保他们发送正确的动作。

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 中不返回 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
            )
          
     

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

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

那是因为我们试图在 .then 子句中的承诺被履行或拒绝后做出断言。但是 .then 不起作用,因为我们只能在 promise 上调用 .then。由于我们忘记返回动作创建器中返回承诺的最后一个嵌套函数,因此我们将在未定义时调用.then。之所以未定义,是因为函数范围内没有return语句。

所以总是在动作创建者中返回函数,当调用时返回承诺。

【讨论】:

【参考方案2】:

您可能希望考虑使用 DevTools - 这将使您能够清楚地看到正在调度的操作以及调用之前/之后的状态。如果调度从未发生,则可能是 fetch 未返回 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 实际上需要两个单独的函数,一个用于成功,一个用于错误。

【讨论】:

这是在测试期间。代码运行良好 - 我只想知道如何测试它。 啊,对不起-我相信这是因为在您的 .then 中您只有一个功能-只有在成功的情况下才会调用它(200响应);您需要在它之后添加另一个函数来处理错误状态。我将更新我的答案以反映这一点。 我试过这个,但这不是实际代码的问题。是考试的问题。我不确定它在哪里,但我认为我没有正确测试我的异步操作

以上是关于使用 redux thunk 测试异步操作的主要内容,如果未能解决你的问题,请参考以下文章

Redux-thunk 异步操作:使用自定义中间件进行异步操作

如何使用 Redux Thunk 链接动态系列的异步操作?

如何使用 redux-axios-middleware 测试 Redux 异步操作

在 redux 中使用 thunk 中间件与使用常规函数作为异步操作创建者相比有啥好处? [关闭]

redux-thunk:错误:动作必须是普通对象。使用自定义中间件进行异步操作

使用 redux-thunk 进行异步验证