Redux TDD: a deep dive
This is a description of the redux-tdd project and how its API works. If you’re looking for a more high-level explanation of test-driven development with redux, please check out my freeCodeCamp article.
Redux TDD is a simple library that let’s you test React components along with the Redux data-flow in a test-driven development (TDD) fashion.
After several iterations of the API, I came up with a simple abstraction that concentrates less on the implementation details of your components or state, and more on the actions dispatched and the props your components should receive:
import reduxTdd from 'redux-tdd';
reduxTdd({ counter: counterReducer }, state => [
<Counter
onClick={incrementAction}
counter={state.counter.count} />
])
.action(props => props.onClick())
.toMatchProps({ counter: 1 })
.contains(<span>1</span>)
In this piece of code above we are shallow rendering (from enzyme) the Counter component, and passing it props which are mapped to a specific part of the state.
The counter prop is mapped to the state value state.counter.count. The state => []
function acts in fact not only as our render function, but also as our mapStateToProps function.
As first argument reduxTdd takes our reducers and therefore the shape of our state.
Next we call .action(props => props.onClick())
which is where the magic happens. Anything returned by the callback passed to the action operator will essentially be dispatched (even though we’re not using redux’s dispatch — more on this later).
The action operator will therefore update the internal state and also update the props of our components based on such new state.
toMatchProps and contains simply check at this point that our component receive the correct values after the action has been dispatched.
TodoList example
In the next example we’ll try to implement a simple TodoList in a TDD fashion — what else 😄?
We start with simply passing our not-yet-existing TodoList component to reduxTdd:
reduxTdd({}, state => [ <TodoList /> ])
Our list items will probably live in state, so let’s go ahead next and introduce a list
reducer, and pass that along to our component as a prop called “listItems”.
reduxTdd({ list }, state => [ <TodoList listItems={state.list} /> ])
Running this will of course fail, since nothing is yet implemented, but let’s write more tests before implementing our logic.
A todo-list is meant to have an input-box where you add todos so let’s also add an AddTodo component (remember we’re returning an array so we can test multiple components):
reduxTdd({ list }, state => [
<TodoList listItems={state.list} />,
<AddTodo onAdd={addTodo} />
])
Great now let’s continue the dot-chaining from the above example and actually simulate the addition of a todo by calling the onAdd
action creator:
.it('should add todo items')
.switch(AddTodo) // the next dot-chained calls will work on AddTodo
.action(props => props.onAdd('clean house')) // add 'clean house'
So next we should be seeing the “clean house” todo in the TodoList, right? Let’s test that:
.switch(TodoList) // back to TodoList
.toMatchProps({ listItems: ["clean house"] })
We can even add another todo:
.switch(AddTodo).action(props => props.onAdd('water plants'))
.switch(TodoList).toMatchProps({ listItems: [
"clean house",
"water plants"
]})
Great that’s enough for now, let’s run this code and start implementing our components, reducers and actions:
const TodoList = ({ listItems }) =>
<div>{listItems.map(item => <div>{item}</div>)}</div>const AddTodo = ({ onAdd }) =>
<button onClick={onAdd}>add todo</button>
(hint: the text-input logic is purposely left out of AddTodo)
Our addTodo action:
function addTodo(todoText) {
return { type: 'ADD_TODO', payload: todoText }
}
And finally our reducer:
function list(state = {}, action) {
switch (action.type) {
case 'ADD_TODO':
return { ...state, [action.payload]: {} }
default:
return state
}
}
If we run our code again some things will fail:
TypeError: listItems.map is not a function
This is because our state.list is an object, but the listItems prop expects an array. Let’s fix that using a selector (you can even use reselect if you want) ❤️
In our initial reduxTdd definition let’s change the way state maps to props for our TodoList component:
reduxTdd({ list }, state => [
<TodoList listItems={getVisibleItems(state)} />,
And the selector:
function getVisibleItems(state) {
return Object.keys(state.list).length
? Object.keys(state.list).map(key => key)
: []
}
Now if we run our tests again we get:
TodoList
✓ should add todo items (1ms)
🎉
Let’s also TDD the idea of “setting a todo item as complete”.
For this I’m thinking there should be a TodoItem component that is clickable right? Let’s add it to our initial list:
reduxTdd({ list }, state => [
<TodoList listItems={getVisibleItems(state)} />,
<AddTodo onAdd={addTodo} />,
<TodoItem onClick={completeTodo} />
])
But there’s a problem! The TodoItem must know which item it must complete. And it must work with a single item, but in our state we keep an object! To solve this we can simply pass a single item from our state.list object:
reduxTdd({ list }, state => [
<TodoList listItems={getVisibleItems(state)} />,
<AddTodo onAdd={addTodo} />,
<TodoItem onClick={completeTodo} item={state.list['water plants']}
/>
])
Next we mark it as complete:
.switch(TodoItem)
.action(props => props.onClick('water plants'))
At this point we realize that our TodoList component has no way of showing whether the item is complete or not (there should be a striked line over it). So we pass a new “completedItems” prop which is an array of the items that are complete:
.switch(TodoList)
.toMatchProps({
listItems: ["clean house", "water plants"]
completedItems: ["water plants"]
})
And we change again our definition to work for such behavior:
reduxTdd({ list }, state => [
<TodoList
listItems={getVisibleItems(state)}
completedItems={getCompletedItems(state)} />,
...
If we run our tests, they tell us some things need to be implemented like our completeTodo action creator:
function completeTodo(todoText) {
return { type: 'COMPLETE_TODO', payload: todoText }
}
And we update our reducer accordingly:
function list(state = {}, action) {
switch (action.type) {
case 'ADD_TODO':
return { ...state, [action.payload]: {} }
case 'COMPLETE_TODO':
return { ...state, [action.payload]: { status: 'complete' } }
default:
return state
}
}
Again running our tests will tell us our getCompletedItems is missing:
function getCompletedItems(state) {
return Object.keys(state.list)
.map(item => state.list[item])
.filter(item => item.status === 'complete')
}
And we’re done!
We can go further and use other operators like contains and view to test more fine-grained parts of our UI. But the main parts of our app have been implemented step-by-step in a TDD way.
More importantly we didn’t have to come up with intricate engineering decisions about our state or our props beforehand. We implemented them as we went ahead and as we saw the need.
I hope these examples helped you understand the idea behind Redux TDD and how you can use it for your own projects. To look at more complex examples please have a look at the tests/ folder inside the repo. You’ll find another important operator called epic which can be used to test async behavior!
Thanks and happy TDD 🤗