Redux: Unit testing smart components

Created on 20 Aug 2015  ·  19Comments  ·  Source: reduxjs/redux

I was reading through the unit testing section of the documentation and while it has an example of how to test a dumb component, being able to unit test a "smart component" (those that use the connect() method) isn't covered. It turns out that unit testing a smart component is a bit more complicated because of the wrapper component that connect() creates. Part of the problem is that wrapping a component via connect() requires that there be a 'store' prop (or context).

I took a crack at trying to do this and I was hoping to get a little feedback on whether there's a better way to accomplish it. And if what I've done does end up looking reasonable, I figured I'd push up a PR to add some info on this into the unit testing documentation.

To start, I took the existing example component in the unit test section of the docs, and wrapped it in connect() to pass in data from state and dispatch-bound action creators:

Header.js (smart component)
import React, { PropTypes, Component } from 'react';
import TodoTextInput from './TodoTextInput';
import TodoActions from '../actions/TodoActions';
import connect from 'redux-react';

class Header extends Component {
  handleSave(text) {
    if (text.length !== 0) {
      this.props.addTodo(text);
    }
  }

  render() {
    return (
      <header className='header'>
          <h1>{this.props.numberOfTodos + " Todos"}</h1>
          <TodoTextInput newTodo={true}
                         onSave={this.handleSave.bind(this)}
                         placeholder='What needs to be done?' />
      </header>
    );
  }
}

export default connect(
  (state) =>  {numberOfTodos: state.todos.length},
  TodoActions
)(Header);

In the unit test file it looks similar to the example as well.

Header.test.js
import expect from 'expect';
import jsdomReact from '../jsdomReact';
import React from 'react/addons';
import Header from '../../components/Header';
import TodoTextInput from '../../components/TodoTextInput';
const { TestUtils } = React.addons;

/**
 * Mock out the top level Redux store with all the required 
 * methods and have it return the provided state by default.
 * @param {Object} state State to populate in store
 * @return {Object} Mock store
 */
function createMockStore(state) {
  return {
    subscribe: () => {},
    dispatch: () => {},
    getState: () => {
      return {...state};
    }
  };
}

/**
 * Render the Header component with a mock store populated
 * with the provided state
 * @param {Object} storeState State to populate in mock store
 * @return {Object} Rendered output from component
 */
function setup(storeState) {
  let renderer = TestUtils.createRenderer();
  renderer.render(<Header store={createMockStore(storeState)} />);
  var output = renderer.getRenderedOutput();
  return output.refs.wrappedInstance();
}

describe('components', () => {
  jsdomReact();

  describe('Header', () => {
    it('should call call addTodo if length of text is greater than 0', () => {
      const output = setup({
        todos: [1, 2, 3]
      });
      var addTodoSpy = expect.spyOn(output.props, 'addTodo');

      let input = output.props.children[1];
      input.props.onSave('');
      expect(addTodoSpy.calls.length).toBe(0);
      input.props.onSave('Use Redux');
      expect(addTodoSpy.calls.length).toBe(1);
    });
  });
});

I've simplified this test a bit to just show the relevant parts, but the main point I'm unsure about is the createMockStore method. If you try and render the Header component in without any props, an error is thrown by Redux (or react-redux) saying that the component must have a store prop or context since it expects to be a child of the <Provider> component. Since I don't want to use that for my unit tests, I created a method to mock it out and allow the test to pass in the state it wants set in the store.

The benefit that I can see of this approach is that it allows me to test the functions within my component, but also be able to test the functionality of the methods I'm passing into connect(). I could easily write another assertion here that does something like expect(output.props.numberOfTodos).toBe(3) which verifies that my mapStateToProps function is doing what I expect.

The main downside of it is that I'm having to mock out the Redux store, which isn't all that complex, but it feels like it's part of the internal Redux logic and might change. Obviously for my unit tests I've moved these methods into a general unit test utility file so if the store methods did change, I'd only have to modify my code in one place.

Thoughts? Has anybody else experimented with unit testing smart components and found a better way of doing things?

discussion question

Most helpful comment

@ernieturner I think what @ghengeveld meant is if you use ES6 modules everywhere, the decorated Header component still could be exported as the default, and a plain React component could be an additional named export. For example -

//header.js
export class HeaderDumbComponent {
  render() {
    return <header><div>...</div></header>;
  }
}

export default connect(
  (state) =>  {numberOfTodos: state.todos.length},
  TodoActions
)(HeaderDumbComponent);

With this dual export, your main app can consume the smart component as before with import Header from './header.js' while your tests can bypass redux and test the behaviour of the core component directly with import {HeaderDumbComponent} from '../components/header.js'

All 19 comments

The alternative is to also export the (undecorated) Header class, so it can be imported and tested separately.

Also you don't need the jsdomReact call in every file, at least if you're using Mocha. Here's my setup.js:

import jsdom from 'jsdom';
import ExecutionEnvironment from 'react/lib/ExecutionEnvironment';

if (!global.document || !global.window) {
  global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
  global.window = document.defaultView;
  global.navigator = window.navigator;

  ExecutionEnvironment.canUseDOM = true;

  window.addEventListener('load', () => {
    console.log('JSDom setup completed: document, window and navigator are now on global scope.');
  });
}

It's loaded via the --require command line option: mocha -r babelhook -r test/setup --recursive (babelhook is a call to require('babel-core/register')).

Ya, I could export both the Header class and the decorated one, but I was hoping to avoid having to change the source code just for my unit tests. Doing that would then cause me to have to change all the places that include a decorated component (e.g. import {DecoratedHeader} from './components/Header' instead of just import Header from./components/Header`).

As for the jsdom setup, I was purely just copying the example from the docs as a way to show the use case, I'm not using that as my main setup, just an example.

I just noticed that my code sample above was incomplete. In order to test a smart component directly, you have to have some utility method return you the refs.wrappedInstance instead of just the result of rendering since that will give you the connect-decorated component (updated in the setup function above). This again kinda makes the unit tests depend on Redux internals (in this case, specifically the react-redux internals). So it works, but feels a bit fragile.

@ernieturner We also offer getWrappedInstance() public API for this so you can rely on it if you're worried about accessing refs directly. And there are utilities like react-test-tree for making this transparent, too.

@ernieturner I think what @ghengeveld meant is if you use ES6 modules everywhere, the decorated Header component still could be exported as the default, and a plain React component could be an additional named export. For example -

//header.js
export class HeaderDumbComponent {
  render() {
    return <header><div>...</div></header>;
  }
}

export default connect(
  (state) =>  {numberOfTodos: state.todos.length},
  TodoActions
)(HeaderDumbComponent);

With this dual export, your main app can consume the smart component as before with import Header from './header.js' while your tests can bypass redux and test the behaviour of the core component directly with import {HeaderDumbComponent} from '../components/header.js'

@eugene1g @ghengeveld This is actually a really good solution to the problem. Feel free to send a PR to the "Writing tests" doc explaining it!

@eugene1g That's exactly what I meant.
@gaearon I'll do that.

@ghengeveld Apologies for the misunderstanding. I'm still using CommonJS style imports so I'm pretty rusty on ES6 modules still. That does seem like a good solution to expose in the docs. I suppose you could also expose the mapStateToProps/mapDispatchToProps methods as well to allow those to be tested without having to deal with mocking out the store, e.g.

export class HeaderDumpComponent ...

export function mapStateToProps(state) ... 
export function mapDispatchToProps(dispatch) ...

export default connect(mapStateToProps, mapDispatchToProps)(HeaderDumpComponent);

It's quite possible most of map methods would be simple enough to not necessitate unit testing, but I have a few of them would be worth verifying since they're such a crucial part of the functionality of my components.

Thanks for the suggestions everyone, if you need any help with the documentation, let me know.

We're trying the recommended approach as outlined above right now, but we don't feel entirely comfortable with it. This leaves the parameters passed to connect untested outside of integration tests. I understand that a mock store could be potentially brittle, but I feel like this is where something like redux-mock-store could jump in, if it could support Component testing. How do you feel about that direction?

@carpeliam

Why not just use a regular store and hydrate it with a particular initial state?
You only need to mock store if you want to test which actions are being dispatched.

@carpeliam

Please see unit tests for counter and todomvc examples in this repo.

We were having an issue where we were rendering a smart component inside of another component and couldn't use the shallow renderer (component used componentDid* methods). What we did was stub (using sinon) the connect function to return a function that returned a simple React component.

It's brittle and we hope to move to the shallow renderer once we can migrate to React 0.14, but this method unblocked us on our tests for the time being.

I'm having some trouble with this as well. I'm rather new to both React and Redux alike so this is likely what's holding me back. How were you able to get around it? @songawee

The proposed double export method leaves the select (mapStoreToState) function untested. In order to test it independently it needs to be exported as well, yet another change in the name of testing.

I'd be interested in finding a way to get shallowRenderer to work with connect's wrapped component. My current trouble is that when using shallow renderer it only passing back the Connect component, which is to be expected.

The proposed double export method leaves the select (mapStoreToState) function untested. In order to test it independently it needs to be exported as well, yet another change in the name of testing.

Not strictly “in the name of testing”. In fact we encourage you to define selector functions (which is what mapStateToProps ultimately is) alongside your reducers, and test them together. This logic is independent of UI and doesn’t have to be bundled with it. Take a look at shopping-cart example for some selectors.

So @gaearon, are you suggesting to get all the data from state using this _selector_ functions? Doesn't that introduce a lot of unneeded overhead, since in most cases people will just read a bunch of properties from state and assign them to props on the components?

Yes, the general suggested pattern for Redux is to use selector functions pretty much everywhere, rather than accessing state.some.nested.field directly. They can be very simple "plain" functions, but are most commonly put together using the Reselect library, which provides memoization capabilities.

Why would that create any extra overhead?

I've been doing the double export as described here but reading the connect source I realized the "Container" keeps a reference to the "dumb" component" in the WrappedComponent static property:

So instead of:

// header.js
export const HeaderDumbComponent = (props) => <header><div>...</div></header>
export default connect(mapStateToProps)(HeaderDumbComponent)


// header.spec.js
import { HeaderDumbComponent } from './header'

it('renders', () => {
  expect(<HeaderDumbComponent />).to.not.be.null
})

You can avoid the double export by using the WrappedComponent where you need the "dumb" version:

// header.js
const HeaderDumbComponent = (props) => <header><div>...</div></header>
export default connect(mapStateToProps)(HeaderDumbComponent)


// header.spec.js
import Header from './header'

it('renders', () => {
  expect(<Header.WrappedComponent />).to.not.be.null
})

@gaearon - WrappedComponent seems to be a pretty stable property given it's in the docs. Would you advise against using it in this way for any reason?

What I do is export the unwrapped component, and also export mapStateToProps and mapDispatchToProps so I can unit test those functions. Here is an example:

import {ManageCoursePage, mapStateToProps, mapDispatchToProps} from './ManageCoursePage';
describe("mapStateToProps", () => {
    function setup(courses, authors, id) {
        const state = {
            authors: authors
        };

        const ownProps = {
            params: {
                id: id
            }
        };

        return mapStateToProps(state, ownProps);
    }

    it('sets the course when a course id is set', () => {
        const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
        const authors = [];
        const id = "1";

        const props = setup(courses, authors, id);

        expect(props.course).toBe(courses[0]);
    });

    it('sets the course when a course id is not set', () => {
        const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
        const authors = [];

        const props = setup(courses, authors, null);

        expect(props.course).toEqual({});
    });

    it('sets the course when a course id is set that is not present in the courses list', () => {
        const courses = [ { id: "1", foo: "bar" }, { id: "2", foo: "notbar" } ];
        const authors = [];
        const id = "42";

        const props = setup(courses, authors, null);

        expect(props.course).toEqual({});
    });

    it('sets the authors formatted for a drop down list', () => {
        const courses = [];
        const authors = [ { id: "1", name: "John" }, { id: "2", name: "Jill" }];

        const props = setup(courses, authors, null);

        expect(props.authors).toEqual([
            { value: "1", text: "John" },
            { value: "2", text: "Jill" }
        ])
    });
});
Was this page helpful?
0 / 5 - 0 ratings

Related issues

ms88privat picture ms88privat  ·  3Comments

mickeyreiss-visor picture mickeyreiss-visor  ·  3Comments

olalonde picture olalonde  ·  3Comments

caojinli picture caojinli  ·  3Comments

ramakay picture ramakay  ·  3Comments