フロントエンドテスト誰がやる?

はじめに

この記事はestie QAメンバー3名によるブログリレーとして最終日のお届けとなります。

1日目、2日目の記事もぜひご覧ください!

QAエンジニアがデータ品質に向き合いはじめた話 - estie inside blog

QAエンジニアとしてのパフォーマンス改善への向き合い方 - estie inside blog


こんにちは!estie QAエンジニアの村上槙 (murashi) です。

今回は、私が所属する開発ユニットの中で取り組んだフロントエンドテスト導入についてお話しします。

フロントエンドのテストは、「何をどのレイヤーで、どこまで書くか」がとても悩ましい領域だと思います。

特に、プロダクトが発展途上のフェーズではUIの変更や仕様の変動が激しく、どこまでテストにコストをかけるべきかは判断が難しいところです。

この記事では、そんな中で私が実践した

  • システム構造を踏まえたテスト戦略の立て方
  • フロントエンドテストのデバッグのつらさと向き合い方
  • テストをチームの“文化”にするためのガイドライン整備

といった取り組みを、QAエンジニア視点でご紹介します。

フロントエンドテスト導入の背景

まず、そもそもなぜフロントエンドテストを導入するに至ったかというと、率直にフロントエンド起因のインシデントが散発したためでした。

私が所属する開発ユニットは、SaaSプロダクト(estie 案件管理)を開発しています。

また、これは管理系SaaSあるあるだと思うのですが、プロダクトの成長に伴い線形的にフロントエンドで取り扱うデータと状態の複雑さが増しています。

加えて、プロダクトフェーズが立ち上げからグロースへ移り変わるタイミングでは、使い勝手や保守性向上のためのデザインアップデートやリファクタリングも重要になります。

ただ、複雑度の増加と大幅なリファクタリングはやはり相性が悪く、インシデントの散発に繋がってしまいました。こうした状況の中で、開発速度を維持しつつ品質の土台を作るためにできることの一つとして、フロントエンドテストを導入しました。

フロントエンドテスト戦略

どのテストも同じではありますが、特にフロントエンドテストを考えるにあたって重要なことは「何に対してテストを書くか」だと考えます。

どのコンポーネントに対して書くか、hooks単体で書くか、統合して書くかなど、テストスコープがばらけるとテストの粒度が揃わず無駄・重複の多いテストになってしまいます。

この辺りを考えるにあたってはフロントエンドテストのテスト戦略で有名なテストトロフィーを参考にしてテストスコープを決めました。テストスコープを決めるにあたっての詳細は後述します。

また、決めたスコープに対して「どれぐらいテストを書くか」も大事なポイントです。私たちのユニットでは以下を基本方針としました。(※本ブログでは、以降結合テストを対象として記述します)

  • テストの価値がある箇所に絞って書く
    • 動作が自明で不具合を拾う確率が低いところは書かない
      • 静的コンテンツの検証、など
  • エラー系のテストも書く
  • 書くことよりも持続可能な計画を目指す
    • Cursor等AIで楽するための計画を立てる

三番目の方針は”どれぐらい”と直接は関係ありませんが、テストを腐らせないための工夫として重要です。なお、方針を決める上で参考にした資料も記載しておきます。

過剰テスト中毒とエラーテスト欠乏症 - UIテスト二大疾病の根治療法 - Speaker Deck

Web フロントエンドのテストと持続可能な方針の組み立てを考える | Offers Tech Blog

システム構造からテストスコープを考える

まず、フロントエンドテストはフロントエンドの各要素(UIコンポーネントやhooks、関数)を仮想DOM上にレンダリングして実施するテストです。なので、テストスコープを決めるためには、そもそもフロントエンドシステムがどういう構造で成り立っているか、を理解する必要があります。estieではNext.jsを採用しており、以下の構造となっています。

project/
├── features/ ✅テスト対象
│   ├── featureA/
│   │   ├── hooks/
│   │   │   └── useHook.tsx
│   │   ├── HeaderComponent.tsx
│   │   ├── SidePanelComponent.tsx
│   │   ├── InputFormComponent.tsx
│   │   └── index.tsx
│   ├── featureB/
│   └── shared/
│       └── Form.tsx
├── utils/(単体テスト対象)
│   └── calc.tsx
└── pages/ ❌テスト対象外


この構造と基本方針を踏まえ、テストスコープの方針は以下としました。

  • features以下の機能コンポーネントに対してテストを書く
    • pagesは動作が自明なのでテスト対象外
  • 原則として <feature>/index.test.tsx に機能全体単位のテストを置く
  • コンポーネントの規模によっては、モックデータが複雑になるためサブコンポーネント単位に分割して書く
    • ただし、その場合でもコンポーネントとの連携確認は必要なため上位コンポーネントのindex.tsxに対してもテストは必要
  • UI単体のテストは書かない
  • 対象コードと同じディレクトリにテストを配置し、近接性と保守性を持たせる

また、テスト観点別に親コンポーネント or 子コンポーネントどちらで見るかも整理するとより迷いが減るのでおすすめです。私の所属ユニットでは以下としています。

テスト観点 親コンポーネントで見る 子コンポーネントで見る
状態の切り替え(表示/非表示など)
コンポーネント間の連携(propsの受け渡し、UI反映)
ユーザー操作に対する機能の一貫した流れ(正常系)
フロー中のエラー処理(404、500、バリデーションエラー) ✅ or 子に委譲 ✅(詳細な表示ロジック)
入力値のバリデーション(未入力、文字数など) ❌(結果だけ見るならOK)

フロントエンドテストのデバッグは沼

前提:私のユニットではフロントエンドテストにVitest + Testing Libraryを使用しています。

テスト設計・実装を通り越していきなりデバッグの話をします。というのも、所感ではありますが、フロントエンドテストにおいて一番沼にハマるのはテストコードのデバッグ時だからです。

具体的には、「なぜテストコードがうまく動かないか分かりづらい」と私自身はかなり感じました。E2Eテストであれば、テストコード通りにブラウザが操作されるのが実際に目に見えるので、どのステップでFailしたかや、その時のUIやAPIリクエストがどういう状態であったかも分かるのでデバッグはかなりやりやすいと思います。(原因不明のFailを除き)

一方、フロントエンドテストでは仮想DOM上でユーザインタラクションを再現できるものの、操作が目に見えるわけではなくDOM情報とError messageのみがテストの結果として出力されます。また、DOM情報もTesting Libraryの文字数制約上限によってかなり省略されてしまうので、大事なところが見えずデフォルトのエラー出力だけでは役に立たないことが多いです。

以降で私が感じたデバッグ時の困り事と対処方法を一部ご紹介できればと思います。

コンソールのエラー出力が見づらい

まず、エラー出力が見づらい。この辛みはシンプルが故の辛さだなと思います。特に、複数のテストでFailした時はコンソールにかなりの量のエラー出力が表示されるので辿るだけで地味に大変でした。当初は、テスト前に一度コンソールクリアしてテスト動かすことで、可能な限り辿りやすくしていたりしましたが、人間は残念なことにすぐ忘れてしまうもの。。

こういう時は、VitestのUIモードを使うことがおすすめします。

UIモードと言っても、Playwrightのようにブラウザの動きが見えるわけではないのですが、以下のメリットがあります。

  • Failの理由やConsole logがテスト単位で見れる(コンソールより圧倒的に見やすい)
  • テストコードのどの部分でFailしたか分かる
  • 特定テストだけの実行もUI上から簡単にできる
  • テストのre-runがコード変更を検知して勝手に走る(ホットリロード)

個人的には、テスト修正のためにコマンド実行せずにすむありがたさから、ホットリロード機能推しです。

DOM状態が省略されて確認したいところが見れない

上述した通り、Testing Libraryの制約でDOM情報の出力が省略されてしまう結果、必要な情報が得られないことがままありました。

こういう時は、まずgetByfindByでUI要素を絞ってscreen.debug(element)することをおすすめします。

また、どうしても全DOM出力したい場合はscreen.debug(undefined, Infinity)で出力することも可能ですがDOMサイズによっては時間がかかるので要注意です。

各UI要素の値がどうなってるか広めに確認したい

特定のタイミングで複数のUI要素がどういう値を持ってるか確認したいが、目に見えるUIがないのでかなり分かりづらいです。一つ一つDOM出力する or 全DOM出力して確認すれば分かるものの不要な情報も多いのでこれはこれで面倒に感じます。

こういう時は、以下のデバッグコードで確認したいUI要素の値のみに絞って確認ができます。

screen.getAllByRole('spinbutton').forEach((input) => {
      console.log(`${input.getAttribute('name')}: ${(input as HTMLInputElement).value}`);
});

出力イメージは以下です。

numberA: 84.69
numberB: 279.97
ratio: 80.1
year: 2020
month:

ガイドラインの整備

実装時の工夫点や困りごとは、個人の中に留めておかずチーム内誰でもアクセス可能な状態にしておくことが重要です。また、個人的にはルールではなくガイドラインとして作成し、迷った時に見ると良いものとすることが文化づくりには重要だと考えています。

というのも、ルールとして強制することで生まれる統一感より、各人が最善だと思うことを自由にできる文化の方がテストを書くハードルが下がり、結果としてテストを書く文化ができると思うためです。

とはいえ、自由度が高すぎると人によって各テストがバラけすぎてしまうので、ガイドラインでは以下に分類して整備しました。

分類 定義
Required 絶対に守ってほしい基本原則。
Should 強い推奨。可能な限り守る。
Recommended 複数の選択肢があった場合のデフォルト。Tips的なものも含む

例えば、各分類には以下のようなガイドを記載しています。

分類 ガイドライン
Required
  • testファイル(*.test.ts|tsx)はソースファイルと同じファイル名で同階層に配置する
Should
  • 要素の取得にはfindBy...を使用する
Recommended
  • 対象コンポーネントのアサーションはwithinで絞ってから実施する
  • 新しくコンポーネントにテストを追加する際はテストシナリオをconfluenceにまとめる(更新時は不要)

ガイドラインには、Recommendedに記載したようなテスト実装時のポイント以外も含めています。

ガイドライン作成にあたって参考にした資料はこちらです。
テストコードのガイドライン 〜作成から運用まで〜 - Speaker Deck

まとめ

私自身、estieに入社して初めてフロントエンドテストの戦略立てから実装、ガイドライン整備までやりました。正直、これまではフロントエンドテストはソフトウェアエンジニアの持ち物、と割り切ってあまりタッチしてこなかったですが、品質への効果としても個人の学びとしてもやって良かったです。

余談ですが、estieのPodcast estie inside FM #23の中で登場する、QAの人が作ったコンフルは本ブログで紹介したガイドラインのことです。自分が作ったものがソフトウェアエンジニアの営みにも活かされることは嬉しい限りです。

最後に

estie では高速な開発の中でも手段問わず品質を高めることに対して魂を燃やせる QAエンジニア を大募集しています。少しでも興味をお持ち頂けたらぜひカジュアル面談へのご応募、お待ちしております!

hrmos.co

hrmos.co

© 2019- estie, inc.