简体   繁体   中英

unit testing a react component with mocha

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", ie 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:

  1. the component state should now have an editing flag associated with it
  2. the element should have changed, and TodoItem.js should 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.

liComponent.props is undenfined because liComponent is a DOM element, not a react component. That's the case because you're fetching it with findRenderedDOMComponentWithTag . You actually already have access to the React component you're trying to test against.

it('should render the element', () => {
    const liComponent = TestUtils.findRenderedDOMComponentWithTag(component, 'li');

    // `component` is from your `before` function
    expect(component.state.editing).toBe(false);

    // Make sure you're simulating on a DOM element
    TestUtils.Simulate.doubleClick(liComponent);

    expect(component.state.editing).toBe(true);
});

You can then use scryRenderedComponentsWithType to check whether a TodoInput is rendered.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM