最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Is there an idiomatic way to test nested state branches? - Stack Overflow

programmeradmin1浏览0评论

So let's say that I have a reducer which has several branches, but each branch is similar enough to generate with a factory function. So, I create one:

import { bineReducers } 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 bineReducers({ contents, meta })
}

Which I use to pose a larger root-level state tree:

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

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

That should give me a state graph which would look like this:

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

If I wanted to test this state, my first instinct is to create a version of this scope in my test suite, and then test that isolated reducer (just asserting against the contents, and meta branches).

The plication here is that I'm trying to also test selectors, and all of the selector designs I've read seem to suggest these two things:

  1. You encapsulate your state by colocating selectors and reducers in the same file.
  2. mapStateToProps should only need to know two things, the global state and a relevant selector.

So here's my problem: Pairing together these more or less posed reducers with root-level-concerned selectors has made my tests a little thick with boilerplate.

Not to mention that hard-writing selectors with knowledge of the entire tree feels like it defies the point of the reducer modularity attempt.

I'm 100% sure I'm missing something obvious, but I can't really find any example code which demonstrates a way to test modular reducers and selectors.

If a selector generally should know the entire global state, but you have reducers which are highly posed, is there a clean, idiomatic approach to testing that? Or maybe a more posable selector design?

So let's say that I have a reducer which has several branches, but each branch is similar enough to generate with a factory function. So, I create one:

import { bineReducers } 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 bineReducers({ contents, meta })
}

Which I use to pose a larger root-level state tree:

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

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

That should give me a state graph which would look like this:

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

If I wanted to test this state, my first instinct is to create a version of this scope in my test suite, and then test that isolated reducer (just asserting against the contents, and meta branches).

The plication here is that I'm trying to also test selectors, and all of the selector designs I've read seem to suggest these two things:

  1. You encapsulate your state by colocating selectors and reducers in the same file.
  2. mapStateToProps should only need to know two things, the global state and a relevant selector.

So here's my problem: Pairing together these more or less posed reducers with root-level-concerned selectors has made my tests a little thick with boilerplate.

Not to mention that hard-writing selectors with knowledge of the entire tree feels like it defies the point of the reducer modularity attempt.

I'm 100% sure I'm missing something obvious, but I can't really find any example code which demonstrates a way to test modular reducers and selectors.

If a selector generally should know the entire global state, but you have reducers which are highly posed, is there a clean, idiomatic approach to testing that? Or maybe a more posable selector design?

Share Improve this question edited Jun 5, 2016 at 22:14 Dan Abramov 268k87 gold badges416 silver badges518 bronze badges asked Jun 5, 2016 at 21:57 ironchamberironchamber 2042 silver badges10 bronze badges
Add a ment  | 

1 Answer 1

Reset to default 14

The point of colocating selectors and reducers is reducers and selectors in one file should operate on the same state shape. If you split reducers into multiple files to pose them, you should do the same for your selectors.

You can see an example of this in my new Egghead series (videos 10 and 20 might be especially useful).

So your code should be more like

const createList = (type) => {
  const contents = ...
  const meta = ...
  return bineReducers({ 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) => ...

Then, your index.js might look like

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

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

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

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

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

This way the root selectors delegate to the child selectors, just like the root reducers delegate to the child reducers. In every file, state inside selectors corresponds to the state of the exported reducer, and state shape implementation details don’t leak to another files.

Finally, for testing those reducer/selector bundles you could do something like

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)
  })
})
发布评论

评论列表(0)

  1. 暂无评论