ReactのテストをVitestのIn-Source Testingに寄せると体験良いので紹介させて欲しい!

2023/12/18に公開

はじめに

先日Vitest v1.0.0がリリースされました 🎉

記念してこの記事では私の推しである、Vitest の In-Source Testing を用いて Private Method だけをテストする手法を紹介させてください!

フロントエンドフレームワークに依存した話ではありませんが、React を例に紹介します。
明確なトレードオフがありどこでも採用できるものではないですが、一つの選択肢として参考にしていただければと思います。以下箇条書きとなります。

モチベーション

フロントエンドテストの大変さ

  • Component にしろ hooks にしろ下記に直面する
    • Context Provider のモックが大変
    • API のモックが大変
    • テストのために、下記のようなライブラリを使うのが一般的で実行や学習コストが高くなりがち
  • その割にテストでは useEffect のテストなどを満足にできないケースが多い
    • 特に router に依存したテストは難しい
  • E2E テスト※ との境界が難しい
    • 権限周りなど含め Provider や MSW でモックしだすと、E2E テストの領域と重なっていく
    • そしてそれらは実行もメンテもコストが大きい

こうした背景から、Web フロントエンドのテストに対して、私個人はコスパ悪く感じてしっくりきたことがなかった。

※ 当記事では Playwright 等を用いて Browser context を作り、end-to-end で行うテストのことを E2E テストとしている。

Private Method をテストできる In-Source Testing の存在

  • 一般的に Private Method はテストするべきでないと言われてきた
    • Private Method は実装の詳細で独立して存在することはなく Public Method をテストすれば十分
    • テストのために Private Method を Public にしたり変なハックを加えるのは避けるべき
  • 一方で私は下記の流れも感じている

Vitest In-Source Testing の紹介

セットアップは公式ドキュメントを参照。vite.config と tsconfig に加筆するだけ。

以下要点絞った React での In-Source Testing 例。

const uneasyLogic = (data: T) => {
  // some logic
  return null;
};

export const useSomething = () => {
  const { data: rawData } = useQuery(~);
  const data = useMemo(() => uneasyLogic(rawData), [rawData]);

  return {
    data,
  };
};

if (import.meta.vitest) {
  const { it, expect } = import.meta.vitest;
  const mockData: T = {};

  it("uneasyLogic", () => {
    expect(uneasyLogic(mockData)).toBe(null);
  });
}

ポイント

  • テストするために Vitest 以外のライブラリや環境に依存していない
  • Private Method を export することなくテストできる
  • API(Service Worker)や Provider や hooks などの関数をモックする必要がなく、data オブジェクトをテストしたい部分だけモックすれば良い
  • 不安な部分にのみテストを書けて、コロケーションに優れたドキュメントにもなる

In-Source Testing と E2E の組み合わせの提案

上記の In-Source Testing による Private Method のテストと、Playwright 等による E2E テストのみでテストする組み合わせを推したい。
つまり Component に対するテストも hooks に対するテストも書かず、Testing Library を利用しない構成。
React で実践するポイントは下記。

  • Component にロジックを直接書かないようにする
    • hooks で生成(取得と加工)したデータを Component ではレンダリングするだけのイメージ
    • hooks 内で不安なロジックは React に依存しない関数として切り出し In-Source Testing する
  • data を信頼する前提があるので API を型安全にする仕組みを用意する
    • GraphQL, OpenAPI スキーマから型の生成や、RSC や tRPC(zod) による Universal TypeScript の活用など
    • 型の信頼が前提なので、サーバー(API)側のテストは従来どおり必要
  • React のレンダリングを信頼する前提なので、どう動作するか理解してから書く

トレードオフ

  • 良い点
    • 開発者体験がとても良い
      • 不安な部分だけテスト書けて、実行が早くて、テストに対するモックも最小限
    • テストが実装の近くにあるので、コロケーションの文脈でよい
      • ドキュメンテーションの役割も期待できる
    • Testing Library や MSW に依存しないことで開発時の実行速度やメンテしやすさに繋がる
  • 考慮が必要な点
    • E2E テストが膨らみやすい
    • テストコードの有無に個人差やバラツキが生じやすい(カバレッジの基準は目指しにくい)
    • Component や hooks にテスト書きたくなる人は現れそう
      • 設計に対する制約を決めるか、部分的にテストを許容するかといったルールが必要になる
    • コードの行数が増える。テストコードによって行数が 10 倍になることも
      • プロダクトコードには混ざらず、エディタで折り畳めて、必ず最下部に記述するので、個人的には気にならないが

ポイント

  • 事前にテスト戦略に対するチームの合意が必要
    • 権限周りのテストなどはどうするか、E2E テストはどれくらい膨らむ想定か、カバレッジはどれくらい必要かなど
  • React Query などを用い、永続化や状態変更のロジックを API が抱えるアプリケーションでは、Vitest の In-Source Testing と E2E だけで十分なテストカバレッジを確保できると感じる
    • クライアントは query で取得したデータを表示用に加工するのが役割なので、加工ロジックに対してテストを書けば良い
  • 反対にあえて Redux を用いるようなクライアントの状態ヘビーなアプリケーションに対して、Vitest の In-Source Testing と E2E の構成は適してないと感じる

最後に、この記事は atama plus Advent Calendar 2023 の 18 日目でした。
明日は @yukihira1992 の記事です。お楽しみに!

Discussion