ironchamber ironchamber - 5 months ago 11
Javascript Question

Is there an idiomatic way to test nested state branches?

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


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

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

const rootReducer = combineReducers({ 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 complication 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 composed 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 composed, is there a clean, idiomatic approach to testing that? Or maybe a more composable selector design?

Answer

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 compose 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 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) => ...

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 combineReducers({ 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)
  })
})
Comments