Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 598
ironchamber Avatar asked Jun 05 '16 21:06

ironchamber


People also ask

How are nested states specified?

The rules are that: (1) the nested state targeted by transitions exiting the fork must be in different AND-states, (2) any AND-state region not specified will use its default state, and (3) it is not specified which nested state will be arrived at first, only that by the end of the state machine execution step, all the ...

Why do we need nested states in interaction diagram?

A state can contain other states, often called as nested states or substates. If you are modeling complex state machines, use nested states to separate detailed behavior into multiple levels. States can also contain actions that identify the tasks that can occur when an object is in a particular state.

When there is only one level of nesting the deep and shallow history states are?

When only one level of nesting, shallow and deep history states are semantically equivalent.

What is a nested diagram?

A nested state diagram is used to model the complex system as the regular state diagram is inadequate in describing the large and complex problem. The nested state diagram is the concept of advanced state modelling. Conventionally a complex system has much redundancy.


1 Answers

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)
  })
})
like image 199
Dan Abramov Avatar answered Sep 24 '22 03:09

Dan Abramov