Description
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?