もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

enzymeでReact RouterのwithRouterで包んだRedux Containerのテストする

  • 今の仕事はReact/ReduxなSPAをTypeScriptで書いてて、HOCのテストの書き方ちょっと迷ったのでメモとして残しておきます。

関連する主なライブラリ

  • "react": "~16.0.0",
  • "react-dom": "~16.0.0"
  • "react-redux": "~5.0.6"
  • "react-router-dom": "~4.2.2"
  • "redux": "~3.7.2"
  • "enzyme": "~3.1.0"
  • "enzyme-adapter-react-16": "~1.0.1"
  • "redux-mock-store": "~1.3.0"

こんな感じ

  • ラップするComponent。コードは適当にURIから受け取ったIDが表示 + ボタンを一つ持つだけのやつ。

components/HogeContent/index.tsx

export interface HogeContentOwnProps extends React.ClassAttributes<HogeContent>;

export interface HogeContentConnectedProps {
  readonly disabled: boolean;
}

export interface HogeContentDispatchProps {
  readonly onClickButton: () => void;
}

export interface HogeContentRouterProps = RouteComponentProps<{id: number}>;

export type HogeContentProps = HogeContentOwnProps & 
  HogeContentConnectedProps & 
  HogeContentDispatchProps & 
  HogeContentRouterProps;

export default class HogeContent extends React.Component<HogeContentProps> {

  constructor(props: HogeContentProps) {
    super(props);
    this.onClickButton = this.onClickButton.bind(this);
  }

  public render() {
    return (this.props.disabled) ? (
      <div>
        {this.props.match.params.id}
      </div>
    ) : (
      <div>
        <button
          type="submit"
          onClick={this.props.onClickButton}
        >
          {this.props.match.params.id}
        </button>
      </div>
    );
  }
}
  • テスト対象となるComponentを包むContainer

containers/Hoge/index.tsx

const mapStateToProps = (state: GlobalState, _: HogeRouterProps): HogeContentOwnProps & HogeContentConnectedProps {
  const { disabled } = state.hoge;
  return {
    disabled
  }
};


const mapDispatchToProps = (dispatch: Dispatch<GlobalState>, props: HogeRouterProps): HogeContentDispatchProps {
  return {
    onClickButton: () => {
        dispatch(sendHoge(););
        props.history.push('/fuga');
    }
  };
};

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(HogeContent));
  • こんな感じで '/hoge' で来たらHogeContentを表示するみたいなやつに対して以下の項目をテストしたいとします。
    • Reduxのstoreからdisabledを渡しているか
    • onClickButtonが押されたらsendHogeをdispatchしているか
    • onClickButtonが押されたら /fuga に遷移しているか
  • なおテストはmochaで走らせてます。
describe('containers', () => {
  describe('<HogeIndex />', () => {
    let wrapper: ShallowWrapper;
    let mockStore: MockStore<{}>;
    let mockRouter: any;

    beforeEach(() => {
      mockStore = configureMockStore()({
        hoge: {
          disabled: false
        }
      });
      mockRouter = {
        route: {
          match: { params: { id: 1 } },
          location: {
            pathname: '/hoge'
          }
        },
        history: {
          push: sinon.stub()
        }
      };

      wrapper = shallow(
        <Hoge
          match={mockRouter.route.match as any}
          location={mockRouter.route.location as any}
          history={mockRouter.history as any}
        />
      ).dive({
        context: {
          router: mockRouter
        }
      }).dive({
        context: {
          store: mockStore
        }
      });
    });

    it('should component received props', () => {
      const actual = wrapper.find(HogeContent).props().disabled;
      assert.equal(actual, false);
    });

    it('should dispatch sendHoge when button clicked', () => {
      const actual = wrapper.find(HogeContent).props().onClickButton();
      const actions = mockStore.getActions();  // configureMockStoreを使うとこのようにDispatchされたActionが取れる
      assert.equal(actions[0].type, 'SEND_HOGE_ACTION');
    });

    it('should go to fuga when button clicked', () => {
      const actual = wrapper.find(HogeContent).props().onClickButton();
      assert(mockRouter.history.push.calledWith('/fuga'));
    });
  });
});
  • Enzymeのdiveを使うとラップされたComponentをshallow renderしたのが返ってくるので、その時にHOCから注入されるContextを設定してあげます。Reduxだけの場合はshallowの2番目の引数にContextを設定できるのですが、withRouterで更に包んだ場合は withRouter -> Router -> connect -> componentという階層になってるので、diveしつつ各階層で必要な情報を注入して上げる感じでテスト対象のComponentをrenderできました。

参考

dive() · Enzyme