10

我正在尝试测试具有异步调用的操作。我使用 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 语句时,什么都不会执行。

我在测试中做错了什么?

4

2 回答 2

30

在 Redux 中测试异步 Thunk 操作

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

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

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

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

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

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

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

动作与动作创建者

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

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

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

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

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

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

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 的总体操作拆分为三个较小的同步操作。我们的主要获取操作创建者是异步的。请记住,这个主要动作创建者并不是动作本身,它只是为了调度进一步的动作而存在。

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

使用操作建模异步操作

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

动作.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。由于我们忘记返回action creator 中最后一个返回promise的嵌套函数,所以我们将在 undefined 上调用 .then。它未定义的原因是因为在函数范围内没有返回语句。

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

于 2017-01-08T17:48:37.330 回答
0

You may wish to consider using DevTools - this would enable you to see clearly the action being dispatched as well as your state before/after the call. If the dispatch never happens then it may be the fetch is not returning a 200 type error.

The promise then should be:

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

And so forth - you will see the .then actually needs two separate functions, one for success and one for error.

于 2016-10-20T23:12:21.467 回答