是否有一种惯用的方法来测试嵌套的状态分支?

时间:2016-06-05 21:57:04

标签: javascript redux

所以,让我们说我有一个具有多个分支的reducer,但是每个分支都足够相似,可以使用工厂函数生成。所以,我创建了一个:

import { combineReducers } from 'redux'

const createReducerScope = scopeName => {
  const scope = scopeName.toUpperCase()

  const contents = (state = {}, action) => {
    switch (action.type) {
      case `${scope}_INSERT`:
        return { ...state, [action.id]: action.item }
      default:
        return state
    }
  }

  const meta = (state = {}, action) => {
    switch (action.type) {
      case `${scope}_REQUEST`:
        return { ...state, requesting: true }
      case `${scope}_REQUEST_FAILURE`:
        return {
          ...state,
          requesting: false,
          errorMessage: String(action.error)
        }
      case `${scope}_REQUEST_SUCCESS`:
        return {
          ...state,
          requesting: false,
          errorMessage: null
        }
      default:
        return state
    }
  }

  return combineReducers({ contents, meta })
}

我用它来组成一个更大的根级状态树:

const products = createReducerScope('products')
const orders = createReducerScope('orders')
const trades = createReducerScope('trades')

const rootReducer = combineReducers({ products, orders, trades })

这应该给我一个状态图,看起来像这样:

{
  products: { contents, meta },
  orders: { contents, meta },
  trades: { contents, meta }
}

如果我想测试这种状态,我的第一直觉就是在我的测试套件中创建这个范围的版本,然后测试那个孤立的reducer(只针对contentsmeta进行断言分支)。

这里的复杂之处在于我试图测试选择器,而我读过的所有选择器设计似乎都暗示了这两件事:

  1. 通过将选择器和缩减器放在同一文件中来封装您的状态。
  2. mapStateToProps应该只需要知道两件事,全局状态和相关选择器。
  3. 所以这就是我的问题:将这些或多或少组合的减速器与根级关注选择器配对在一起使得我的测试有点厚,带有样板。

    更不用说具有整棵树知识的硬写选择器感觉它无法理解还原器模块化尝试的重点。

    我100%确定我遗漏了一些明显的东西,但我无法找到任何示例代码来演示测试模块化缩减器和选择器的方法。

    如果一个选择器通常应该知道整个全局状态,但是你有一个高度组合的reducer,那么有一种干净的,惯用的测试方法吗?或者可能是一个更具组合性的选择器设计?

1 个答案:

答案 0 :(得分:14)

定位选择器和缩减器的重点是 reducers和一个文件中的选择器应该在相同的状态下运行。如果将Reducer拆分为多个文件来组合它们,则应该对选择器执行相同的操作。

您可以在我的new Egghead series中看到相关示例(视频1020可能特别有用)。

所以你的代码应该更像

const createList = (type) => {
  const contents = ...
  const meta = ...
  return combineReducers({ contents, meta })
}

// Use default export for your reducer
// or for a reducer factory function.
export default createList

// Export associated selectors
// as named exports.
export const getIsRequesting = (state) => ...
export const getErrorMessage = (state) => ...

然后,您的index.js可能看起来像

import createList, * as fromList from './createList'

const products = createList('products')
const orders = createList('orders')
const trades = createList('trades')

export default combineReducers({ products, orders, trades })

export const getIsFetching = (state, type) =>
  fromList.getIsFetching(state[type])

export const getErrorMessage = (state, type) =>
  fromList.getErrorMessage(state[type])

这样根选择器委托给子选择器,就像root redurs委托给子reducers一样。在每个文件中,选择器内的state对应于导出的reducer的state,状态形状实现细节不会泄漏到其他文件。

最后,为了测试那些reducer / selector包,您可以执行类似

的操作
import createList, * as fromList from './createList')

describe('createList', () => {
  it('is not requesting initially', () => {
    const list = createList('foo')
    const state = [{}].reduce(list)
    expect(
      fromList.isRequesting(state)
    ).toBe(false)
  })

  it('is requesting after *_REQUEST', () => {
    const list = createList('foo')
    const state = [{}, { type: 'foo_REQUEST' }].reduce(list)
    expect(
      fromList.isRequesting(state)
    ).toBe(true)
  })
})