Some Thoughts On Testing React/Redux Applications

On Component and Reducer Tests, Asynchronous Actions and Generative Testing

TL;DR:

This is how I currently approach testing React/Redux Applications.
  • Minimal component tests: verify that the component actually renders.
  • Avoid testing implementation details.
  • Verify important callbacks and other props, but keep it minimal.
  • The need to test logic inside a component might signal the need for refactoring.
  • Using Eslint, PropTypes and Flow will add value.
  • Test reducers as they are functions. Same data in. Same data out.
  • Action Creators: only indirectly, when testing reducers.
  • Async Action Creators: yes fully tested.
  • React-Redux: focus on selectors when complex logic is involved.
  • Connected components: might be useful as a type of integration test, not a prime focus.
  • Fully test any services, common and utility functions. Should all be functions. Making testing simple as same data in, same data out.
  • Advanced Testing: Use generative or property-based testing when applicable, catch edge-cases. Reducers are predestined for property-based testing.
  • E2E Tests for the critical parts.

Introduction

The following is not intended as a best practice or a collection of “best-of approaches” for testing a React and/or Redux application. Rather a personal guide and reflection on how to approach testing when building a React App. None the less, it might also prove to be valuable for others. If you’re trying to figure out how to get off the ground with testing a React/Redux App this might be a good starting point.


Tooling

Currently there are multiple tooling options for writing tests in JavaScript with Karma/Jasmine and Mocha/Chai being the more mainstream combinations. Furthermore there is a wide range of alternatives like Tape and other frameworks and assertion libraries as well as testing utilities to choose from.

Regarding components, Enzyme is what appears to be the de-facto standard for testing real-world applications. The other option is Jest, that comes with snapshot-testing and improved auto-mock handling. Finally you can also use the low-level React Test Utilities. There is no general rule or recommendation regarding the one or the other. This is up to the developer or team to decide what suits best for a specific project.

Create-React-App now comes with a Jest test setup out of the box. This might be a good entry point if you still need to figure out how to get things off the ground.

In case you need a quick-start for a Mocha/Chai/Sinon/Enzyme setup, then checkout this gist.

For an example on how to setup your tests with Karma/Jasmine/Enzyme read this.

The following component test examples have been written using Enzyme.


Components

Minimal vs. Full Component Tests

A very minimal approach would be to only test if the component did render. Considering how inconsistent components can be, this makes total sense. Changing the class name will break the test, if we start verifying class names for example. Taking into account how quickly component internals change, it’s clear that taking this route doesn’t make much sense*.

(*Jest seems to tackle the problem with snapshot-testing if I’m not mistaken. If anyone can give insights on this, please leave a comment.)

This is how the most minimal but very effective component test would look like a.k.a ”The only React.js component test you’ll ever need”

The only React.js component test you’ll ever need (Enzyme + Chai) https://gist.github.com/thevangelist/e2002bc6b9834def92d46e4d92f15874

Additionally we might also verify important callbacks being triggered via sinon or similar libraries.

it('toggles the completed status of an item when clicking', () => {
const app = mount(<Root />);
const item = app.find('#item-1');
item.simulate('click');
expect(app.find('.completed').length).toBe(1);
item.simulate('click');
expect(app.find('.completed').length).toBe(0);
});

Also testing a certain number of items being rendered when passing in certain props is a valid approach.

it('renders items', () => {        
const props = {items: { id: 1', title: 'foo'}}
const list = shallow(<Release {...props} />);
expect(list.find('.item').length).toBe(1);
});

What we definitely want to do, is avoid testing React itself. A test like the following adds no value, it just confirms that React knows how to handle jsx.

const Header = ({title}) => <h1>{title}</h1>
it('filters out any out of print items', () => {
const headerTitle = shallow(<Header title="foo" />);
expect(headerTitle.find('h1').length).toBe(1);
});

Neglect testing any implementation details like tags, elements or attributes for the sake of testing if they’re being rendered. These details tend to change very quickly and the tests add no real value.

Bonus: If you’re still wondering about how to test any logic inside the component, the best answer is to not have to test any logic in the first place. Components should simply render a given input, logic should remain outside the component whenever possible.

Summary:

  • Minimal component test to verify that the component actually renders.
  • Avoid verifying tags or class names.
  • Verify important callbacks or props, but keep it minimal.
  • The need to test logic inside a component might signal the need for refactoring.

Additional: Using Eslint, PropTypes and Flow will add more value than simply trying to verify any internal details.

For more in-depth on the topic also read Testing in React: Getting Off The Ground.


Testing Redux

Reducers

Testing reducers should be straight forward. Same data in, same data out. Another thing to consider is to use action creators when creating the action for the reducer under test. By taking this approach we don’t need to explicitly test any action creators, as this might involve some overhead without real benefits.

it('should handle ADD_TODO', () => {
expect(
todos([], addTodo('Test AddTodo') // use the action creator
).toEqual([
{
text: 'Test AddTodo',
completed: false,
id: 0
}
])
})

Bonus: Using generative tests to verify reducers. Read this for or a more detailed writeup.

Action Creator

See reducers. No explicit action creator tests. See the following tests, we’re rewriting the action creator to be able to test it.

it('addTodo should create ADD_TODO action', () => {
expect(addTodo('Test addTodo')).toEqual({
type: types.ADD_TODO,
text: 'Test addTodo'
})
})

Even a better example.

const completeAll = () => ({ type: types.COMPLETE_ALL })
expect(completeAll()).toEqual({
type: types.COMPLETE_ALL
})

By creating the actions via action creators when testing reducers, we’re already verifying that the action creators work as expected.

Async Action Creators with Redux-Thunk

Testing includes mocking the store or mocking a specific call. There are a couple of possible approaches, but best is to consult the redux documentation.

The following example is taken straight from redux “Writing Tests” section.

it('creates FETCH_TODOS_SUCCESS when fetching todos has been done', () => {
nock('http://example.com/')
.get('/todos')
.reply(200, { body: { todos: ['do something'] }})

const expectedActions = [
{ type: types.FETCH_TODOS_REQUEST },
{ type: types.FETCH_TODOS_SUCCESS,
body: { todos: ['do something'] }
}]
const store = mockStore({ todos: [] })

return store.dispatch(actions.fetchTodos())
.then(() => { // return of async actions
expect(store.getActions()).toEqual(expectedActions)
})
})

Yes, asynchronous action creators based on redux-thunk for example should be tested when possible.

Async Action Creators with Redux-Saga

Testing asynchronous actions with redux-saga is as simple as calling next on the generated object. Take a look at the shopping-cart test example:

test('getProducts Saga test', function (t) {
const generator = getAllProducts(getState)
let next = generator.next(actions.getAllProducts())

t.deepEqual(next.value, call(api.getProducts),
"must yield api.getProducts"
)

next = generator.next(products)

t.deepEqual(next.value, put(actions.receiveProducts(products)),
"must yield actions.receiveProducts(products)"
)
t.end()
})

We don’t have to deal with mocking api calls and other asynchronous actions, as we only verify if the returned action is the expected one. This also enables us to verify that the actions are returned in the correct order.

Testing React-Redux

No testing of connected components nor verifying that mapDispatchToProps returns expected results at the moment. Regarding the first, I would like to hear some feedback. It could also be seen as a type of integration test, but I would like to hear about the real benefits, besides testing if react-redux is passing the updated state to a given component under test. Regarding testing mapDispatchToProps, under regular circumstances, we’re only composing action creators with dispatch.

In regards to the connect method, mapStateToProps might be interesting to test, especially when defined as a selector. This depends on the fact if logic is involved when selecting the state and might make a lot of sense in given situations.

Like mentioned above, I would like to hear feedback on how you approach testing the react-redux specific parts.


Services and Utility Functions

Services and utility functions used across a project are usually fully tested, as they generally should always be functions. Same data in, same data out. Writing tests using any assertion library should be straight forward to accomplish with this functions only approach.


Advanced: Property-based Testing

Writing generative or property-based tests has a number of advantages, including finding edge cases that might go unforeseen when only focusing on example based tests. Reducers are a good starting point for writing property-based tests.

If you’re interested in understanding how to approach generative testing with JavaScript in general and redux in specific you might want to read this and this.


E2E Tests

This needs a post of its own, but try to test the most critical parts of the application like login, registration etc.


Outro

This should have been a quasi high level summary of one possible approach to test a React/Redux application. I am really interested in feedback on how you approach testing and where I might be mistaken.

In case you have any questions or feedback leave a comment here or leave feedback via twitter.