reduxを使うReact Componentのテスト

2015-11-10T19:59:35+09:00 JavaScript react.js redux

ドハマりしてたけど結局ドキュメントに書いてるっていうオチなメモ

※詳しい事は記事末尾にある参考を参照

src/components/TodoInput.jsx

import React, { Component, PropTypes } from "react";
import LinkedStateMixin from "react-addons-linked-state-mixin";
import { connect } from "react-redux";

class TodoInput extends Component {

  constructor(props) {
    super(props);
    this.linkState = LinkedStateMixin.linkState;
    this.state = {};
  }

  render() {
    return (
      <div>
        <input type="text" valueLink={this.linkState("text")} />
        <button onClick={(e) => this.handleClick(e)}>add</button>
      </div>
    );
  }

  handleClick(e) {
    this.props.onAddTodo(this.state.text);
  }
}

export default connect()(TodoInput);

あくまでReactの事はおいといてreduxのconnectを使ってるコンポーネントをテストする

test/components/TodoInput-test.js

import React from "react";
import ReactDOM from "react-dom";
import ReactTestUtils from "react-addons-test-utils";
//import { createStore } from "redux";

describe("TodoInput", () => {
  it("render", () => {
    var TodoInput = require("../../src/components/TodoInput").default;a

    // 最小限getStateさえあれば良い。subscribe/dispatch無いと警告は出る
    var store = {
      getState: function() {
        return {};
      },
      subscribe: function() {},
      dispatch: function() {}
    };

    // もしくはこれでもいい
    // var store = createStore(() => {});

    let component = ReactTestUtils.renderIntoDocument(<TodoInput store={store} />);
    console.log(ReactDOM.findDOMNode(component).innerHTML);

    // output: <input type="text" data-reactid=".0.0"><button data-reactid=".0.1">add</button>
  });
});

っていうようにconnectを使ってるComponentをテストする場合、そのコンポーネント自身にstoreを指定する必要がある模様

又、テストを開始する際にdocument等の依存が解決出来ないのでtest/setup.jsを

import { jsdom } from 'jsdom'

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

を作っておく必要がある

てな感じでreduxを使うReact Componentのテストを行う際にはstoreを指定する必要があるって事で終わり。一応、まだテストに関するドキュメントを制覇してないので読みつつ色々まとめる予定

参考: http://redux.js.org/docs/recipes/WritingTests.html

余談: React Componentのstateをテストする場合

上記のReact ComponentではlinkStateを使ってstateに状態を持つ事をやってる。そこがただしく作用しているかっていう所までチェックしたい場合、上記のテストコードの末尾に

expect(todo.state.text).toEqual("hoge");

とやっても出来ない。なぜかというとconnectされたReact Componentはreact-reduxのconnectのConnectコンポーネントに内包される形になるので上記のtodo変数自体はTodoInputではなくConnectになる。もちろんTodoInput内でのsetStateはTodoInput上で作用するので問題無いがテスト上ではそのstateにアクセスする事が出来ない。それをやるには

  • connectを使うコンポーネントのオプションにwithRef: trueを設定する
  • 一定の処理後にgetWrappedInstanceメソッドで内包されているComponentのインスタンスを取る
  • stateを検証する

っていうのが必要になる。以下に例を示す

import React, { Component, PropTypes } from "react";
import LinkedStateMixin from "react-addons-linked-state-mixin";
import { connect } from "react-redux";

class TodoInput extends Component {

  static propTypes = {
    onAddTodo: PropTypes.func.isRequired
  };

  constructor(props) {
    super(props);
    this.linkState = LinkedStateMixin.linkState;
    this.state = {};
  }

  render() {
    return (
      <div>
        <input type="text" valueLink={this.linkState("text")} />
        <button onClick={(e) => this.handleClick(e)}>add</button>
      </div>
    );
  }

  handleClick(e) {
    this.props.onAddTodo(this.state.text);
  }
}

export default connect(
  (state) => {
    return state;
  },
  null,
  null,
  // これが必要
  { withRef: true }
)(TodoInput);

んでテストする際には上記で書いたように普通に使ってもこれはConnectクラスに内包されてしまうのでgetWrappedInstanceで取得してからstateをテストする

import expect from "expect";
import React from "react";
import ReactDOM from "react-dom";
import ReactTestUtils from "react-addons-test-utils";
import { createStore } from "redux";
import { Provider } from "react-redux";

describe("TodoInput", () => {
  it("render", (done) => {
    var TodoInput = require("../../src/components/TodoInput").default;
    var store = createStore(() => { return {} });

    let onAddTodo = (text) => {
      expect(text).toEqual("hoge");
      done();
    };

    let todo = ReactTestUtils.renderIntoDocument(
      <TodoInput store={store} onAddTodo={onAddTodo} />
    );

    let input = ReactTestUtils.findRenderedDOMComponentWithTag(todo, "input");
    ReactTestUtils.Simulate.change(input, { target: { value: "hoge" }});

    let button = ReactTestUtils.findRenderedDOMComponentWithTag(todo, "button");
    ReactTestUtils.Simulate.click(button);

    let instance = todo.getWrappedInstance();
    expect(instance.state.text).toEqual("hoge");
  });
});

っていう感じ。現在withRefをテストでのみ作用させるにはReact Component自体を動的にconnectで処理させる方法以外無さそう

っていう感じで一応出来る

upgrade-insecure-requests babel6の件