- 今の仕事は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
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
に遷移しているか
- Reduxのstoreから
- なおテストは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できました。