Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React でコンポーネントを利用したテストをゴリゴリ書く

bom-shibuya
November 06, 2023

React でコンポーネントを利用したテストをゴリゴリ書く

2023/11/06 ZOZO Tech Meetup - Webフロントエンド

bom-shibuya

November 06, 2023
Tweet

Transcript

  1. © ZOZO, Inc. 株式会社ZOZO ZOZOTOWN開発1部 WEBフロントエンドブロック 渋谷 拓正 Web 制作や

    CRM サービスの Web アプリの開発に関 わった後、2023年4月に ZOZO に入社。 プロテインを朝晩摂取したり絵を描いたりしています。 子供と遊んで毎日過ごしています。 2
  2. © ZOZO, Inc. https://zozo.jp/ 3 • ファッションEC • 1,500以上のショップ、8,900以上のブランドの取り扱い •

    常時95万点以上の商品アイテム数と毎日平均2,900点以上の新着 商品を掲載(2023年6月末時点) • ブランド古着のファッションゾーン「ZOZOUSED」や コスメ専門モール「ZOZOCOSME」、靴の専門モール 「ZOZOSHOES」、ラグジュアリー&デザイナーズゾーン 「ZOZOVILLA」を展開 • 即日配送サービス • ギフトラッピングサービス • ツケ払い など
  3. © ZOZO, Inc. 5 はじめに • 当時の考え ◦ hook やコンポーネントで使われる関数がそれぞれ正しく動いている

    ▪ コンポーネント内の実装はそれらを組み合わせている部分が大きい • ある程度正しいことが担保されているのでは? ◦ コンポーネントは JSX が書かれている ▪ DOM 構造と結びついているので壊れやすいのでは? • それから3年経って今どのようなことを考えているか ◦ => 今日のお話 🔥
  4. © ZOZO, Inc. 8 こんな経験はありませんか? • Hook のテストは書いたがコンポーネントにあるロジックはテストしていない • 修正のたびにテストも直す必要がある

    ◦ 毎回直さなければならないので、そもそもテストを書くのが面倒に感じる、、、 • 何度も同じようなテストコードを書いている気がする ◦ 別のテストファイルで同じようなテストを書いた気がする、、、
  5. © ZOZO, Inc. 10 public なメソッドをテストする • 実装の詳細をテストしてしまっているのかも • private

    なメソッドはテストを書かない ▪ private なメソッドは public なメソッドを通して必ず利用されているはず ▪ 参考: t-wada 「プライベートメソッドのテストは書かないもの?」 • React にとって public な関数とは? ◦ => コンポーネント ◦ ある機能を満たす形でディレクトリが切られそこから 1 つコンポーネントが export される ▪ 実際に外から使われるこのコンポーネントが public な関数 このコンポーネントをテストすることで内部で使れているコードがテストできるはず💡
  6. © ZOZO, Inc. 11 コンポーネントを通してテストを書く 例えば以下のようなコンポーネント - Articles - index.ts

    // Articlesをexport - Articles.tsx - CategorySelect.tsx - ArticleList.tsx - Article.tsx - useArticles.ts - convertDate.ts • 記事の一覧表示のコンポーネント ◦ SelectBox があり、カテゴリを選択すると記事の一覧を取得する ◦ useArticles は記事の取得などの処理が行われる ◦ convertDate は投稿日時を表示用の形式に変換する処理が記述されている
  7. © ZOZO, Inc. 12 コンポーネントを通してテストを書く • export されているのは Articles コンポーネント

    => Articles.test.tsx のテストをかく ◦ これが public な関数 • Articles コンポーネントをテストすることで useArticles や convertDate の挙動も確認できる ◦ => useArticles.test.ts を別途書く必要はない test("初期表示時、reactの記事を取得するリクエストが送信されること", async () => { // ... }); test("投稿日時が意図した形式で表示されていること", async () => { // ... });
  8. © ZOZO, Inc. 13 コンポーネントを通してテストを書くポイント • 小さめの機能ごとにテストを書いて下位のコンポーネントから動作を担保するイメージ ◦ 大きいコンポーネントは検証項目が多くなりテストが難しくなる ◦

    小さい範囲では網羅的にテストできる ▪ 上位のコンポーネントではそこで担保すべきテストを書くといい • 下位のコンポーネントの内容を再度テストする必要はない • それぞれの責任範囲を意識する • 全ての状態を網羅するのが難しいとき ◦ もしかするとそのコンポーネントは責務を持ちすぎているかもしれない ◦ renderHook などを利用して個別にテストを書く ▪ この場合はコンポーネントを通したテストでは代表的なケースのみにするなどバランス をとる
  9. © ZOZO, Inc. 14 Q: 同じようなテストを何回も書いている気がする A: どこで何を担保するのかを考えてテストを書くといいかも • 先述の

    Atricles の例 ◦ もし convertDate が useArticles で使われている場合 ▪ 両方にテストを書くとかなり似てしまう • プロジェクトの規約的に fetch するコードは一箇所にまとめている ◦ fetch するコードのテストはそのディレクトリで書く ◦ Articles でも SelectBox を変更した時にリクエストを確認するようなテストを書く ▪ それぞれで書くテストはテストしたいことが違う ▪ => チームでテスト方針を相談できると良さそう
  10. © ZOZO, Inc. 15 Q: コンポーネントは DOM 構造と結びついているのでテスト が壊れやすいのでは? A:

    そんなことはなく、むしろ a11y への意識向上に繋がります󰢏 • getByRole のような a11y 属性を利用して要素を取得する ◦ 実装(コンポーネント)の詳細を意識せずににコンポーネントを扱うことができる ▪ => DOM 構造の変更に影響を受けにくい • テストで状態を検証するために WAI-ARIA などを意識することが増える ◦ 例えば tab の切り替えを検証するために aria-selected を用いて状態を表現する ◦ ARIA属性を利用してテスタブルにする ▪ => a11y への意識が向上
  11. © ZOZO, Inc. 17 共通で使われるカスタムフック • 各プロジェクトには共有の Hooks 置き場がある ◦

    複数の箇所から使われるカスタムフック • こういう Hook もコンポーネントを通してテストする ◦ Hook は必ずコンポーネントで使われる ◦ 実際に使ってみることになるので、使い心地がわかる ▪ コンポーネントを通してテストすることで実際の使用方法の例示にもなる 次のページでサンプルコンポーネントを用意したのでみてみましょう
  12. © ZOZO, Inc. 18 共通で使われるカスタムフック const Component = () =>

    { const { onCategoryChange, isLoading, error, articles } = useArticles({ initialCategory: CATEGORY.react }); const changeAngular = () => { onCategoryChange(CATEGORY.angular);}; return ( <div> <button type="button" data-testid="changeAngular" onClick={changeAngular} /> {isLoading && <div data-testid="loading" />} <ul> {articles.map((article) => (<li data-testid="article" key={article.id}>{article.title}</li>))} </ul> {error != null && <div data-testid="error" />} </div> ); }; • 動作確認に必要な最小限の機能のコンポーネント ◦ button なども data-testid で引っ張れる様にしている(getByRole は若干遅いらしい)
  13. © ZOZO, Inc. 19 共通で使われるカスタムフック test("記事情報の取得中isLoadingはtrueになり、取得後はfalseになる", async () => {

    const articles = generateMockArticles({ category: "react", length: 3 }); server.use( rest.get(buildEndpoint("react"), (req, res, ctx) => { return res(ctx.json(articles)); }) ); render(<Component />); expect(screen.getByTestId("loading")).toBeInTheDocument(); expect(await screen.findByTestId("loading")).not.toBeInTheDocument(); expect(screen.getAllByTestId("article").length).toBe(3); }); • getByTestId メインで利用して要素を掴んでいる • renderHook だと loading 状態のテストが難しいがコンポーネントだと書くことができる
  14. © ZOZO, Inc. 20 Q: カスタムフックではない関数のテストもコンポーネント でテストするべき? A: 共通の関数などは普通に単体テストで良い •

    コンポーネントと密接に結びついているのであればコンポーネントからテストできる ◦ そうでないなら普通にテストするでOK • 例えばどんな関数? ◦ getServerSideProps ◦ バリデーション関係 ◦ fetch のラッパー関数 ◦ などなど
  15. © ZOZO, Inc. 22 まとめ • React のテストすべき public な関数はコンポーネント

    ◦ export されているコンポーネントを通してテストを書く ◦ 小さめの機能ごとにテストを書いて下位のコンポーネントから動作を担保 • コンポーネントを利用したテストで a11y への意識向上に繋がる • どこで何を担保するのかを考えてテストを書く • カスタムフックもコンポーネントを利用してテストを書く ◦ Hook は必ずコンポーネントで使用される ◦ 使用方法の例示になる
  16. © ZOZO, Inc. 23 色々話してきましたが、 • どんなテストであってもテストがまったくないよりはあった方が良い ◦ 不要になれば捨てたら良い ▪

    プロダクションコードには影響がない ◦ 書かないと掴めない部分もある ▪ RSC の登場で考え方が変わる可能性もある • テストは自分が書いたコードが意図通りに動いているかを確認するためのもの ◦ 将来の変更で意図しない変更がないことを担保するためのもの ◦ コードの仕様を把握しやすくするためのもの ▪ 「〇〇の時はxxになる」 最後に