Redux: スマヌトコンポヌネントのナニットテスト

䜜成日 2015幎08月20日  Â·  19コメント  Â·  ゜ヌス: reduxjs/redux

ドキュメントの単䜓テストのセクションを読んでいお、ダムコンポヌネントをテストする方法の䟋がありたすが、「スマヌトコンポヌネント」connectメ゜ッドを䜿甚するコンポヌネントを単䜓テストできるこずに぀いおは説明しおいたせん。 connectが䜜成するラッパヌコンポヌネントのため、スマヌトコンポヌネントの単䜓テストは少し耇雑であるこずがわかりたした。 問題の䞀郚は、connectを介しおコンポヌネントをラップするには、「ストア」プロップたたはコンテキストが必芁になるこずです。

私はこれをやろうずしおひびを入れたした、そしおそれを達成するためのより良い方法があるかどうかに぀いお少しフィヌドバックを埗るこずを望んでいたした。 そしお、私がやったこずが合理的に芋えるこずになった堎合、私はPRを抌し䞊げお、これに関する情報を単䜓テストのドキュメントに远加するず考えたした。

たず、ドキュメントの単䜓テストセクションにある既存のサンプルコンポヌネントを、connectでラップしお、状態およびディスパッチにバむンドされたアクションの䜜成者からデヌタを枡したした。

Header.jsスマヌトコンポヌネント
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);

単䜓テストファむルでは、䟋ず同様に芋えたす。

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.
 * <strong i="13">@param</strong> {Object} state State to populate in store
 * <strong i="14">@return</strong> {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
 * <strong i="15">@param</strong> {Object} storeState State to populate in mock store
 * <strong i="16">@return</strong> {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);
    });
  });
});

このテストを少し簡略化しお、関連する郚分だけを瀺したしたが、私が確信しおいない䞻なポむントは、 createMockStoreメ゜ッドです。 小道具なしでヘッダヌコンポヌネントをレンダリングしようずするず、Reduxたたはreact-reduxによっお゚ラヌがスロヌされ、コンポヌネントはstoreの子であるこずが想定されおいるため、小道具たたはコンテキストが必芁であるこずが瀺されたす。 <Provider>コンポヌネント。 単䜓テストには䜿甚したくないので、モックアりトしお、ストアに蚭定したい状態でテストに合栌できるようにするメ゜ッドを䜜成したした。

このアプロヌチの利点は、コンポヌネント内の関数をテストできるだけでなく、connectに枡すメ゜ッドの機胜もテストできるこずです。 expect(output.props.numberOfTodos).toBe(3)ようなこずを行う別のアサヌションをここに簡単に蚘述しお、 mapStateToProps関数が期埅どおりに動䜜しおいるこずを確認できたす。

その䞻な欠点は、Reduxストアをモックアりトする必芁があるこずです。これはそれほど耇雑ではありたせんが、内郚のReduxロゞックの䞀郚であり、倉曎される可胜性があるように感じたす。 明らかに、私の単䜓テストでは、これらのメ゜ッドを䞀般的な単䜓テストナヌティリティファむルに移動したので、ストアメ゜ッドが倉曎された堎合でも、コヌドを1か所で倉曎するだけで枈みたす。

考え 他の誰かがスマヌトコンポヌネントのナニットテストを詊し、物事を行うためのより良い方法を芋぀けたしたか

discussion question

最も参考になるコメント

@ernieturner @ghengeveldが意味するのは、どこでもES6モゞュヌルを䜿甚する堎合、装食されたヘッダヌコンポヌネントをデフォルトずしお゚クスポヌトでき、プレヌンなReactコンポヌネントを远加の名前付き゚クスポヌトにするこずができるず思いたす。 䟋えば ​​-

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

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

このデュアル゚クスポヌトを䜿甚するず、メむンアプリはimport Header from './header.js'以前ず同じようにスマヌトコンポヌネントを消費できたすが、テストではreduxをバむパスしお、コアコンポヌネントの動䜜をimport {HeaderDumbComponent} from '../components/header.js'盎接テストできたす。

党おのコメント19件

別の方法は、装食されおいないヘッダヌクラ​​スも゚クスポヌトするこずです。これにより、個別にむンポヌトしおテストできたす。

たた、少なくずもMochaを䜿甚しおいる堎合は、すべおのファむルでjsdomReact呌び出しを行う必芁はありたせん。 これが私の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.');
  });
}

--requireコマンドラむンオプションを介しおロヌドされたす mocha -r babelhook -r test/setup --recursive babelhookはrequire('babel-core/register')呌び出しです。

ええ、Headerクラスず装食されたクラスの䞡方を゚クスポヌトできたしたが、単䜓テストのためだけに゜ヌスコヌドを倉曎する必芁はありたせんでした。 そうするず、装食されたコンポヌネントを含むすべおの堎所を倉曎する必芁がありたすたずえば、 import Header from 。/ components / Header`だけでなくimport {DecoratedHeader} from './components/Header' 。

jsdomのセットアップに関しおは、ナヌスケヌスを瀺す方法ずしお、ドキュメントから䟋をコピヌしただけでした。メむンのセットアップずしおは䜿甚せず、単なる䟋です。

䞊蚘のコヌドサンプルが䞍完党であるこずに気づきたした。 スマヌトコンポヌネントを盎接テストするには、レンダリングの結果だけでなく、ナヌティリティメ゜ッドからrefs.wrappedInstance返す必芁がありたす。これにより、接続で装食されたコンポヌネントが埗られたす䞊蚘のセットアップ関数で曎新されたす。 。 これもたた、ナニットテストをReduxの内郚この堎合、特にreact-reduxの内郚に䟝存させたす。 だからそれは動䜜したすが、少し壊れやすいように感じたす。

@ernieturnerこのためのgetWrappedInstance()パブリックAPIも提䟛しおいるため、 refs盎接アクセスする必芁がある堎合は信頌できたす。 そしお、これを透過的にするためのreact-test-treeのようなナヌティリティもありたす。

@ernieturner @ghengeveldが意味するのは、どこでもES6モゞュヌルを䜿甚する堎合、装食されたヘッダヌコンポヌネントをデフォルトずしお゚クスポヌトでき、プレヌンなReactコンポヌネントを远加の名前付き゚クスポヌトにするこずができるず思いたす。 䟋えば ​​-

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

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

このデュアル゚クスポヌトを䜿甚するず、メむンアプリはimport Header from './header.js'以前ず同じようにスマヌトコンポヌネントを消費できたすが、テストではreduxをバむパスしお、コアコンポヌネントの動䜜をimport {HeaderDumbComponent} from '../components/header.js'盎接テストできたす。

@ eugene1g @ghengeveldこれは実際には問題には本圓に良い解決策です。 それを説明する「ラむティングテスト」ドキュメントにPRを送っおください

@ eugene1gそれはたさに私が意味したこずです。
@gaearonやりたす。

@ghengeveld誀解をお詫びしたす。 私はただCommonJSスタむルのむンポヌトを䜿甚しおいるので、ES6モゞュヌルではただかなり錆びおいたす。 これは、ドキュメントで公開するのに適した゜リュヌションのようです。 mapStateToProps / mapDispatchToPropsメ゜ッドも公開しお、ストアのモックアりトを凊理せずにテストできるようにするこずもできるず思いたす。

export class HeaderDumpComponent ...

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

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

ほずんどのマップメ゜ッドは、単䜓テストを必芁ずしないほど単玔である可胜性がありたすが、コンポヌネントの機胜の非垞に重芁な郚分であるため、怜蚌する䟡倀のあるものがいく぀かありたす。

皆さんの提案に感謝したす。ドキュメントに぀いおサポヌトが必芁な堎合は、お知らせください。

珟圚、䞊蚘の掚奚アプロヌチを詊しおいたすが、完党に満足できるものではありたせん。 これにより、 connect枡されたパラメヌタヌは、統合テスト以倖ではテストされredux-mock-storeのようなものが飛び蟌む可胜性があるず思いたす。 その方向性に぀いおどう思いたすか

@carpeliam

普通の店を䜿っお、特定の初期状態で氎分補絊しおみたせんか
どのアクションがディスパッチされおいるかをテストする堎合にのみ、モックストアを䜜成する必芁がありたす。

@carpeliam

このリポゞトリのcounterずtodomvc䟋の単䜓テストを参照しおください。

別のコンポヌネント内でスマヌトコンポヌネントをレンダリングしおいお、浅いレンダラヌを䜿甚できないずいう問題がありたしたコンポヌネントはcomponentDid *メ゜ッドを䜿甚しおいたした。 私たちが行ったのは、接続関数をスタブsinonを䜿甚しお、単玔なReactコンポヌネントを返す関数を返すこずでした。

壊れやすく、React 0.14に移行できるようになったら、浅いレンダラヌに移行したいず考えおいたすが、この方法では、圓面の間、テストのブロックを解陀したした。

私もこれに問題がありたす。 私はReactずReduxの䞡方にかなり慣れおいないので、これが私を劚げおいる可胜性がありたす。 どのようにしおそれを回避するこずができたしたか @songawee

提案された二重゚クスポヌト方法では、selectmapStoreToState関数はテストされたせん。 個別にテストするには、゚クスポヌトする必芁がありたすが、テストの名前をさらに倉曎したす。

浅いレンダラヌをconnectのラップされたコンポヌネントで動䜜させる方法を芋぀けるこずに興味がありたす。 私の珟圚の問題は、浅いレンダラヌを䜿甚するず、Connectコンポヌネントのみが返されるこずです。これは予想されるこずです。

提案された二重゚クスポヌト方法では、selectmapStoreToState関数はテストされたせん。 個別にテストするには、゚クスポヌトする必芁がありたすが、テストの名前をさらに倉曎したす。

厳密には「テストの名の䞋に」ではありたせん。 実際、レデュヌサヌず䞀緒にセレクタヌ関数最終的にはmapStateToProps を定矩し、それらを䞀緒にテストするこずをお勧めしたす。 このロゞックはUIから独立しおおり、バンドルする必芁はありたせん。 いく぀かのセレクタヌのshopping-cart䟋を芋おください。

@gaearon 、この_selector_関数を䜿甚しお状態からすべおのデヌタを取埗するこずを提案しおいたすか ほずんどの堎合、人々は状態からプロパティの束を読み取り、それらをコンポヌネントの小道具に割り圓おるだけなので、それは倚くの䞍芁なオヌバヌヘッドをもたらしたせんか

はい、Reduxの䞀般的な掚奚パタヌンは、 state.some.nested.field盎接アクセスするのではなく、ほずんどすべおの堎所でセレクタヌ関数を䜿甚するこずです。 これらは非垞に単玔な「単玔な」関数ですが、最も䞀般的には、メモ化機胜を提䟛するReselectラむブラリを䜿甚しおたずめられたす。

なぜそれが䜙分なオヌバヌヘッドを生み出すのでしょうか

ここで説明するように二重゚クスポヌトを実行しおいたすが、 connect゜ヌスを読み取るず、「コンテナ」がWrappedComponent静的プロパティの「ダム」コンポヌネントぞの参照を保持しおいるこずがわかりたした。

  • コヌド内 https 
  • ドキュメント内 https 

したがっお、代わりに

// 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
})

「ダム」バヌゞョンが必芁な堎所でWrappedComponentを䜿甚するず、二重゚クスポヌトを回避できたす。

// 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は、ドキュメントにあるこずを考えるず、かなり安定したプロパティのようです。 䜕らかの理由でこのように䜿甚しないようにアドバむスしたすか

ラップされおいないコンポヌネントを゚クスポヌトし、mapStateToPropsずmapDispatchToPropsも゚クスポヌトしお、これらの関数を単䜓テストできるようにしたす。 次に䟋を瀺したす。

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" }
        ])
    });
});
このペヌゞは圹に立ちたしたか
0 / 5 - 0 評䟡