I'm working through a TodoMVC example for the Redux ecosystem. I've completed working code for the example and am now working through the creation of tests for each of the elements of the application.
For actions and reducers, the testing is very straightforward, but for the components, writing tests has proven somewhat more challenging.
My general component architecture looks like this:
Home.js
\-App.js
\-TodoList.js
\-TodoItem.js
\-TodoInput.js
Writing the unit tests for TodoInput.js has been relatively straightforward:
TodoInput.js:
handleChange(e) {
this.setState({ text: e.target.value });
}
...
render() {
return (
<input type="text" autoFocus='true'
className={classnames({
edit: this.props.editing,
'new-todo': this.props.newTodo
})}
value={this.state.text}
placeholder={this.props.placeholder}
onKeyDown={this.handleKeyDown.bind(this)}
onBlur={this.handleBlur.bind(this)}
onChange={this.handleChange.bind(this)}>
</input>
);
}
TodoInput-test.js:
const mockedTodo = {
text: 'abc123',
complete: false
};
it(`should update text from user input`, () => {
const component = TestUtils.renderIntoDocument(
<TodoInput
text = {mockedTodo.text}
editing = {false}
onSave = {_.noop}
/>
);
const inputComponent = TestUtils.findRenderedDOMComponentWithTag(component, 'input');
expect(React.findDOMNode(inputComponent).value).toBe(mockedTodo.text);
TestUtils.Simulate.change(React.findDOMNode(inputComponent), {target: {value: "newValue"}});
expect(React.findDOMNode(inputComponent).value).toBe("newValue");
React.unmountComponentAtNode(React.findDOMNode(component));
});
But for TodoItem.js, testing has been a little trickier.
The render code branches based on whether or not an editing flag has been set on the item:
TodoItem.js:
import React, { Component, PropTypes } from 'react';
import TodoInput from './TodoInput';
import classnames from 'classnames';
export default class TodoItem extends Component {
static propTypes = {
todo: PropTypes.object.isRequired,
editTodo: PropTypes.func.isRequired,
markTodoAsComplete: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired
}
constructor(props, context) {
super(props, context);
this.state = {
editing: false
};
}
handleDoubleClick() {
this.setState({ editing: true });
}
handleSave(id, text) {
if (text.length === 0) {
this.props.deleteTodo(id);
} else {
this.props.editTodo(id, text);
}
this.setState({ editing: false });
}
render() {
const {todo, markTodoAsComplete, deleteTodo} = this.props;
let element;
if (this.state.editing) {
element = (
<TodoInput text={todo.text}
editing={this.state.editing}
onSave={(text) => this.handleSave(todo.id, text)} />
);
} else {
element = (
<div className='view'>
<label onDoubleClick={this.handleDoubleClick.bind(this)}>
{todo.text}
</label>
<input className='markComplete'
type='checkbox'
checked={todo.complete}
onChange={() => markTodoAsComplete(todo)} />
<button className='destroy'
onClick={() => deleteTodo(todo)} />
</div>
);
}
return (
<li className={classnames({
completed: todo.complete,
editing: this.state.editing
})}>
{element}
</li>
)
}
}
I'm a little stumped on how to go about writing a test that, for instance, would verify that a double-click on the component had successfully set the state to editing: true.
Typically, I have my tests divided into two parts, "rendering" and "events", i.e. for TodoItem-test.js:
import React, { addons } from 'react/addons';
import _ from 'lodash';
import expect from 'expect';
const { TestUtils } = addons;
import TodoItem from '../TodoItem';
describe('TodoItem', () => {
const mockedTodo = {
text: 'abc123',
complete: false
};
describe('rendering', () => {
let component;
before(() => {
component = TestUtils.renderIntoDocument(
<TodoItem
todo={mockedTodo}
editTodo={_.noop}
markTodoAsComplete={_.noop}
deleteTodo={_.noop}
/>
);
});
afterEach(() => {
React.unmountComponentAtNode(React.findDOMNode(component));
});
it('should render the element', () => {
const liComponent = TestUtils.findRenderedDOMComponentWithTag(component, 'li');
expect(liComponent).toExist();
});
it('should render text in label', () => {
const labelComponent = TestUtils.findRenderedDOMComponentWithTag(component, 'label');
expect(labelComponent).toExist();
expect(React.findDOMNode(labelComponent).textContent).toEqual('abc123');
});
});
describe('events', () => {
...
});
but in this case, I want to see if double-clicking on the component leads to the following:
- the component state should now have an
editingflag associated with it - the
elementshould have changed, andTodoItem.jsshould now render a<TodoInput/>component instead.
What is the most efficient way to structure a test against this expected behavior? I am thinking that I should do two things:
First, test to see if a double-click on the component adds the expected "editing: true" flag. I am not sure how to do this. If I set up a test as follows:
describe('events', () => {
let component;
let deleteTodoCallback = sinon.stub();
beforeEach(() => {
component = TestUtils.renderIntoDocument(
<TodoItem
todo={mockedTodo}
editTodo={_.noop}
markTodoAsComplete={_.noop}
deleteTodo={deleteTodoCallback}
/>
);
});
afterEach(() => {
React.unmountComponentAtNode(React.findDOMNode(component));
});
it(`should change the editing state to be true if a user double-clicks
on the todo`, () => {
const liComponent = TestUtils.findRenderedDOMComponentWithTag(component, 'li');
// expect the editing flag to be false
TestUtils.Simulate.doubleClick(React.findDOMNode(liComponent));
// expect the editing flag to be true
});
});
how do I go about testing to ensure that the editing flag has been set? liComponent.props.editing returns undefined.
Second, have a context("if the component is editing mode") that tests to make sure that the following has been rendered correctly:
<li className={classnames({
completed: todo.complete,
editing: this.state.editing
})}>
<TodoInput text={todo.text}
editing={this.state.editing}
onSave={(text) => this.handleSave(todo.id, text)} />
</li>
I'm also not sure how I would go about testing this rigorously as well.