FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

Webフロントエンドプロダクト用にテスト戦略を考えた話

はじめに

こんにちは。FLINTERS の生田です。
この記事は FLINTERS ブログ祭りにギリギリ滑り込まさせて頂いた記事です…!! 🙌🙇‍♀

テーマは#技術です。
今回は Web フロントエンドを対象としたテスト戦略について書いてみます。

最近、私達のチームでは Web フロントエンドのプロダクト開発に注力しています。その中で「どのようなテストやるといいか?どのテストに力を入れるか?」といった議論が起こりました。

戦略を立てずにテストを闇雲に自動化・記述・実施などすると、テストの過不足が起きてしまうかもしれません。そこで私達は『テストの目的・範囲・手法・コスト・実施タイミング』などを整理し、テスト戦略として策定しました。

この記事では、その流れやテスト戦略について共有します。

※注意

  • 内容はブログ記事用にぼかして書いています。ご了承ください
  • 現時点では、弊チームは Web フロントエンドテストについて経験値浅の状態です。参考にする場合は鵜呑みにせず、ご自身のプロジェクトにあったテスト戦略を立てることを推奨します

策定までの流れ

1.「テストを書く目的」を整理する

  • まずはテスト対象となるプロダクトの特徴を、事業的な背景込みで捉える
  • 「テストで何を担保したいのか」を整理する

2. 「テスト対象範囲」と「テスト手法」を目的と照らし合わせながら整理する

  • アプリケーション/システムを構成している要素を整理する
  • 「担保したいこと」は「どんなテスト手法で、どの要素をテストすれば」担保できるか?を考える

テストを書く目的の整理

対象プロダクトの特徴

  • Web アプリケーション
  • Next.js 14 系(AppRouter 利用)
  • ブラウザからアクセスされるAPIサーバーがあり、裏側にはRDBがある
  • 事業上、重要なユースケースがある
  • 重要なユースケースには、外部システムとの連携があるものも含まれる
  • PC やモバイル端末など、広いレンジの画面サイズでアクセスされる
  • モバイルユーザーからのアクセスが中心
  • 中長期的に運用され、複数人で開発される
  • 開発期日・リソースはパツパツ寄り

テストで担保したい事柄

担保対象 1. プロダクトの信頼性

  • 重要な機能の提供が滞ると、機会損失やユーザーからの信頼喪失につながり、収益にダイレクトに悪影響が出てしまう
  • そのため「システムが重要なユースケースをユーザーに提供できている」ということが重要
  • プロダクトの どのユースケースが重要かを整理し、担保対象とする (すみませんが具体的な内容は書けません)

担保対象 2. コードの健全性・実装品質

  • 中長期的に、複数人で開発するためコードの健全性や実装品質は重要になってくる
  • コードのリファクタリングやRenovateを用いた依存ライブラリの定期更新は、コードの健全性を保つために積極的に取り入れていく予定
  • これらは既存コードに想定外の影響を及ぼしリグレッションを起こし得るため、そのケアをしたい
    • 特にRenovateによる依存ライブラリの更新は高頻度で起こるため、自動テストを導入し、効率よく安全にライブラリ更新をしたい
  • 自動テストを記述することにより、開発者がテスタビリティの高いコードを書くことに繋がり、プロダクトコードの品質向上・アクセシビリティ向上にも繋がる

テスト対象範囲・テスト手法の整理

構成要素

私達のプロダクトの場合、以下が考えられます:

  1. ライブラリが提供する関数
  2. Utility/ロジックを担う関数/hooks
  3. UI コンポーネント(単体)
  4. UI を組み合わせたコンポーネントやページ(結合)
  5. Web API クライアント(ブラウザ API を使う機能)
  6. API サーバーとホスティング環境
  7. RDB 等での永続化データ
  8. 外部システム API (決済・SNS 連携など)

実施するテスト手法

テスト手法 期待する効能 カバレッジ テスト作成の判断 テスト対象 実施タイミング 記述や実行のコスト 方法(例)
静的解析 バグ削減/コード品質向上/開発効率向上/可読性・保守性向上 ★★★ - 2~5 開発中・CI/CD TS/Linter/Formatter
関数単体テスト バグ削減/コード品質向上/保守性向上 ★★☆ 正しい事が自明でなければ作成 2 開発中・CI/CD Vitest
UI コンポーネント単体テスト バグ削減/例外ケース考慮促進/保守性向上 ★☆☆ ロジックが複雑な場合作成 3 開発中・CI/CD Vitest/Testing Library
画面結合テスト・VRT 画面結合テスト: 画面単位での条件に応じた情報表示や機能提供を担保
VRT: 想定外の影響検出/Renovate とのシナジー/保守性向上
★★★ 画面単位で作成(app 以下 page.tsx ごと) 1~5 開発中・CI/CD 中~高 Playwright,wiremock 等のダミー API サーバ
自動 E2E シナリオテスト 重大なユースケース実現を担保/忠実性の高い検証/リリース効率向上 ★☆☆ 手動 E2E シナリオテストのうち手間がかかるものを順次移植 1~8 デイリー CI Playwright,バックエンドは開発用環境
手動 E2E シナリオテスト 重大なユースケース実現を担保/忠実性の高い検証 ★☆☆ 重要なシナリオで手順作成 1~8 リリース前 人力

※ VRT ... Visual Regression Testingの略
※ カバレッジ ... ここでは、各テスト手法におけるテスト対象に対し、テストを用意する割合を大まかに示したもの

作戦について

対「担保対象 1. プロダクトの信頼性」
  • 「システムが重要なユースケースをユーザーに提供できているのか」をテストで検証する
  • 可能な限りテスト対象範囲や記述/実行コストを抑えつつ、プロダクトの信頼性を担保したい
  • 自動 E2E シナリオテストは初期段階では導入しない。導入工数や記述・実施・保守コストが高い
  • リリース前に「手動 E2E シナリオテスト」を実施する
    • 自動化し辛い「外部システムとの連携を含んだ重要なユースケース」をカバー
    • 実際のエンドユーザー利用を想定したシナリオ・環境で実施することでプロダクト信頼性を担保する
  • 「Web アプリケーションがユーザーに機能を提供できているか」を担保するため、画面結合テスト・VRT に注力する
    • VRT を複数画面幅で実施することで、コアターゲットとなるモバイルユーザーへの提供品質低下を防ぐ
対「担保対象 2. コードの健全性・実装品質」
  • 静的解析・関数単体テストは積極的に導入する
    • コードの健全性・実装品質を担保する
    • 実行・記述・保守面でコスパがよい
  • 画面結合テスト・VRT により、依存ライブラリを更新する場合の安全性を効率よく担保でき、Renovate 活用にもつながる
その他
  • Web フロントエンドの自動テストについて、「UI コンポーネント単体テスト」より「画面結合テスト・VRT」に注力する
  • 「The Testing Trophy」を参考にしている( Write tests. Not too many. Mostly integration.
  • Web フロントエンド開発において UI コンポーネントはほとんどの場合組み合わせて機能する
  • UI コンポーネントの単体テストよりも、それらを組み合わせて機能させた際の挙動を検証する「結合テスト」に注力することを推奨
  • 信頼性とスピード/費用のバランスが良い

テスト手法の詳細

静的解析
  • プロジェクト初期にTypeScriptESLintPrettierを導入
  • (最近はBiomeも注目されている。爆速らしいので検討しても良いかも)
  • Lint のルールは積極的に導入する
関数単体テスト
  • hooksや関数に対してその入出力や挙動を検証する
  • 記述コストや実施コストは基本的に低めなので、ほぼすべての関数に対して実施する
  • ただし、関数の挙動がほとんどライブラリに依存する場合や、正しいことが自明な場合には省略する
  • プロジェクト初期から実施する
// 例
import { describe, expect, it } from "vitest";
import { separateNumByComma } from "./separateNumByComma";

describe("separateNumByComma", () => {
  it("整数指定時、3桁カンマ区切りの数字を文字列で返す", () => {
    expect(separateNumByComma(123_456_789)).toBe("123,456,789");
  });
});
UI コンポーネント単体テスト
  • UI コンポーネントのロジックや表示切り替え、エラー表示などの振る舞いを検証する
  • コンポーネントに依存しないロジックは極力.tsファイルに切り出し、関数単体テストを実行する
  • プロジェクト初期から必要に応じ実施する
  • 画面結合テスト・VRT でカバーできるケースは省略する
// 例
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { Header } from "./Header";

describe("Header", () => {
  it("バナーが描画される", () => {
    render(<Header userData={null} />);
    expect(screen.getByRole("banner")).toBeDefined();
  });
});
画面結合テスト・VRT
  • 画面単位でのテスト(AppRouter のpage.tsx毎を想定)
  • UI コンポーネントをユーザーに見せる用に組み立てたものを対象に表示&挙動を確認する
  • API レスポンスのハンドリングも含めて検証できる
  • 画面結合テストと VRT は基本的には一緒のテストファイルで書く
  • 実施方法
    • Playwrightとモック API サーバー(wiremock)を用いる
    • モックを用いるのは、テストのポータビリティ向上と VRT での誤検出を防ぐため
  • 画面結合テストの内容
    • メルペイフロントエンドのテスト自動化方針2. インテグレーションテストを参考にさせていただきました
    • 基本的に、仕様書をベースにアプリケーションの振る舞いが期待通りかをテストします。実装の詳細は考慮しません。インテグレーションテストが仕様書と対応し、テストコードを見るとアプリケーション挙動がわかるように管理されると理想的です。
      テスト観点は次の通りです。

      • Rendering
        • 画面の初期表示
        • 権限による表示出し分け
        • API レスポンス形式に応じた表示出し分け
        • API エラー時の画面表示
      • Action
        • ユーザー操作の結果に応じた画面の変化
        • フォームサブミット時の API 呼び出しの結果
        • ページナビゲーション
      • Validation
        • フォームのバリデーション

      メルペイフロントエンドのテスト自動化方針より引用

// 例
import { expect, test } from "@playwright/test";

const targetUrl = "/pwreset";

test.describe(`Integration test: ${targetUrl}`, () => {
  // 新しいパスワードを入力し、再設定ボタンをクリックする。
  test("set new password", async ({ page }) => {
    await page.goto(`${targetUrl}?id=123`);
    await page.waitForLoadState();
    // VRT
    await expect(page).toHaveScreenshot({
      fullPage: true,
    });
    await page
      .getByRole("textbox", { name: "新しいパスワード" })
      .fill("PASSWORD");
    await page.getByRole("button", { name: "再設定" }).click();
    await page.waitForLoadState();
    await expect(page).toHaveScreenshot({
      fullPage: true,
    });
    await expect(page.getByText(/パスワードを再設定しました/)).toBeVisible();
  });
});

なぜ画面結合テストでTesting Libraryを使っていないのか?

  • Playwright利用によりオーバーヘッドが生じ、テストの時間が増えてしまうのでは?」
  • Testing Libraryは 2024/04 時点で非同期コンポーネントを十分にサポートしていない
  • Next.js の公式ドキュメントでは、非同期コンポーネントでは E2E テストを推奨している: Building Your Application: Testing | Next.js
  • Since async Server Components are new to the React ecosystem, some tools do not fully support them. In the meantime, we recommend using End-to-End Testing over Unit Testing for async components.

  • 今後、我々のプロダクト上では非同期コンポーネントが多用される想定。まずはPlaywrightを用いて非同期コンポーネントかどうかを問わずにテストできるようにする
  • 非同期コンポーネントのテストに関してとても参考になる記事
  • これらの記事では Container / Presentational パターンを用いる方法も紹介されている
    • 今後、テスト実行時間や安定性などで課題感が出てきた場合には検討するかも
自動 E2E シナリオテスト/手動 E2E シナリオテスト
  • バックエンドも含め、実際にユーザーが用いる環境をほぼ再現しテストを行う
  • 忠実度が高い代わりに実行コストが高いため、重要度の高いユースケースのみを対象に実施する
  • 自動テストについてはプロジェクト終盤〜リリース後に導入の検討をする

まとめ

つらつらと箇条書きで紹介するだけとなってしまいましたが、こんな感じでテスト戦略を策定しました。
私達のチームでは、こういったアーキテクチャに関する決定事項は ADR (Architecture Decision Record)として GitLab リポジトリ上で管理されています。
(今回のはアーキテクチャの話か?と言われるとややびみょいが、開発チーム内での意思決定を記録しておく場としてちょうど良かったというのがある)
今後の開発時に参照されながらテストの記述や実施など進めていくことになります。

「こうして戦略ができました、めでたしめでたし」と言って終わりたいところですが、「このテスト戦略が狙い通りとなるのか」もまだ不透明ですし、チーム・プロダクト・プロジェクトを取り巻く環境は刻一刻と変化します。
そのため、将来、立てた戦略の練り直しが頻発することだと思います。
(実際、すでに画面結合テスト・VRT がちょっと時間掛かるな〜とか思い始めてたり…)

この記事を読む場合、テスト戦略そのものよりも「戦略を構築するプロセス」に着目して頂ければ幸いです。

参考文献

感謝申し上げます。