Generative Testing Redux: Reducers

Property-based Testing by Example.

A. Sharif
JavaScript Inside
5 min readAug 18, 2016

--

Introduction

This is a followup to Generative Testing in JavaScript which was intended as a high level introduction into the topic. If you haven’t heard of generative testing or need a reminder you might find reading the introductory part to be helpful. but it is not a prerequisite for this write-up.

The following is a short write-up on how to test a redux application with property-based tests. All examples are based on testcheck but can be quickly adapted to any generative testing framework.

Reading through the official redux test documentation and running the tests inside the examples folder can be helpful for a clearer understanding of what we’re testing but are optional. Furthermore there seems to have been a discussion regarding the topic, but I wasn’t able to find any concrete examples or an implementation that would describe how to use property-based tests to run against your reducers.

Thinking about generative testing and redux

Here are a couple of ideas to play around with, before moving on to the first real attempt at writing a property-based test.

The simplest example, and the one that made redux famous, is the Elm-inspired counter example. Here’s the code to remind us what’s happening inside the counter reducer.

export default function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

The initial state of counter is 0 and we have 2 action types, that either increment or decrement the counter. How would a property look like regarding this reducer?

// first ideaconst result = ({state, action, expected})
=> reducer(state, action) === expected

We’re interested in testing the state transformation, meaning comparing the result of an input state and action with an expected new state. The counter example is still comprehensible enough to try to tackle the problem.

Here are a couple of ideas to think about.

// 1.randomize the state
// describe the shape of the state
const initialState = gen.int
// 2. randomize actions
const randomizeAction = (action, gen) => action(gen)

Now that we have an initial high level idea, let’s try a naive implementation.

We need to calculate the expected output state depending on the generated state and action but we don’t want to reimplement the reducer itself. Here is a scrapped idea that came up while trying to understand how to tackle defining an expectation.

const getExpectedState = ({state, action}) => ({
action,
state,
expected: action.type === 'INCREMENT' ? state + 1 : state -1,
})

Create a getExpectedState function that should accept an object containing state and action and extending this given object with a calculated expected result for verifying the state output.

This implementation will not really add any benefit, as we’re actually reimplementing the reducer itself. If you notice we’re either incrementing or decrementing according to the passed in action type. If we had added an undefined action type to our generated actions, then we would already be copying the internal counter reducer implementation, maybe even ending up with using a switch case. So this is obviously not the way to solve this.

Some more Thinking about generative testing and redux

What do we need, to be able to verify that a reducer is always returning the correct output state?

Next try.

We removed the getExpectedState function all together. So what’s going on here? Instead of mapping over a generated object containing the randomized state and action, we defined expected functions for every possible generated action.

inc for example returns the action and a function, that expects state and returns the expected new state. This still looks like we’re reimplementing the counter reducer, but it’s more explicit, as each action has a corresponding expected function, which then returns a new result based on a passed in state. This works for the counter example, let’s see if we can get this to work with the redux todomvc example.

This approach holds true for the addTodo action in the todomvc example. Now let’s extend this example to see if we can randomize the other actions too.

There’s a lot going on here. We’ve generated a random initial state according to a given shape and created random actions and their corresponding expectation depending on that initial state. While this already works and can help to validate reducers against a large number of cases, there are still a couple problems with the current implementation.

First and foremost we’re still duplicating logic inside our expectations and secondly we can’t guarantee every action is a valid one, meaning that you might have certain async actions that can’t be dispatched when a previous action has been dispatched. Just think about async action creators and how certain actions can only be dispatched when the app has a certain state.

If say, you’re application has a state loading and certain actions might not be able to be dispatched as they depend on another action being dispatched before that, chances are high we might be dispatching incorrect actions.

Refinements

The generated tests are valid, but randomizeDeleteTodo looks a lot like the todo equivalent reducer code.

const randomizeDeleteTodo = id => [deleteTodo(id), state 
=> filter(todo => todo.id !== id, state)]

We will keep the idea of creating pairs of actions and expected functions, only that our expected function should actually be a property that verifies a certain state/action combination return the expected new state. Time to redefine those expected functions, to be able to write predicate functions like this:

(state, nextState) => state.length !== nextState.length

This approach needs to gain access to state as well as the nextState and validate that a given action triggers the correct state transition. This looks a lot clearer now, as opposed to the previous implementations.

What we really want to do is pass in a property function, generate random state and actions and verify that the updated state is valid.

check(property(generatedData, propertyFn(reducer)), options)

The propertyFn function can be as trivial as: expect a reducer and return a new function expecting state and the action/property pair, which passes the state and the new state to the property function.

const propertyFn = reducer => ({state, def: {action, property}}) 
=> property(state, reducer(state, action))

Implementing the high level wrapper might look something similair to this.

// helper functionconst createAction = (action, property) => ({action, property})const propertyFn = reducer => ({state, def: {action, property}}) 
=> property(state, reducer(state, action))
const run = (reducer, generatedData, options)
=> check(property(generatedData, propertyFn(reducer)), options)

Here is the updated todomvc example leveraging the run function.

See the working example.

This needs more work obviously.

More refinements

We still need to deal with ensuring that the correct actions are being dispatched and we need a way to figure out how to write meaningful properties. Finally creating an api for writing your generative reducer test would be highly valuable. In the next post we will need to tackle one and two and three is only a bonus.

For the refinement part, we will focus on the async and shopping-cart examples. We will have a new situation, having to deal with actions being dispatched in the correct order or at least in a non-conflicting way. We will use those examples to write functions that validate if a certain action is applicable.

Don’t hesitate to provide feedback or ask questions in case something is unclear or if there are better alternatives to the solutions presented here. The focus should be on gaining insights on how to test the store with a random set of actions and a generated initial state, guaranteeing a correct order of actions.

Feedback or questions via twitter

Links

todoMvc Example

Generative Testing in JavaScript

Redux

--

--

A. Sharif
JavaScript Inside

Focusing on quality. Software Development. Product Management.