Closed11

フロントエンド開発テスト入門

Yusuke InaiYusuke Inai

README

読んでくぞ。携わっているプロダクトで今後必ず必要になってくる。

https://github.com/frontend-testing-book

https://zenn.dev/ganezasan/articles/0b361b56e2fb7e

サンプルコードにどうやら力を入れてるみたいなので、コードを読んで今携わっているプロダクトへ落とし込んでいきたい。
あとはどのような場面ではどのテスト手法を選ぶべきなのか?という疑問も解決していきたい。

Yusuke InaiYusuke Inai

第1章 テストの目的と障壁

Q. そもそものテストを書く目的とは...?

  1. UIやシステム関連の故障は、イメージダウンにつながり事業に経済的な影響を及ぼす
    →BFF開発では、認証認可などのバグが含まれていてはやばいものも多くある(フロントエンドはこの領域ももはや担当する)

  2. 健全なコードの維持
    →共通化したり、リファクタリングをしたいときなどに実装済みの機能に影響を及ぼしてしまうかもしれないという不安が生じてできない...!
    →安心感与えてくれる
    他にも、Dependabotによりアップデートをこまめに行えるような状態を作れるようになる。自動テストが書かれていれば「マイナーアップデートの場合、テストがパスしていればマージOK」というルールが設けられる

  3. 実装品質に自信を持つため
    →テストコードを書くことで、自分が書いたコード品質を見直すいい機会になる
    テスト対象のコードが書きづらいと感じたら、それはテスト対象に処理を詰め込みすぎているサイン
    例)肥大化したUIコンポーネント。
    表示分岐、入力バリデーション、非同期処理更新...といった様々な処理がUIコンポーネントには実装されている。
    →これらを1つのUIコンポーネントで実装すると、どういったテストを書くべきか悩む
    →単一責任原則に則り、分割するだけでも実装も整理されるしテストも書きやすくなる
    他にも、Webアクセシビリティにも配慮していきたい。昨今のUIコンポーネントテストでは、アクセシビリティ由来の要素取得APIを使ってテストコードを書く機会が増えてきている。もし要素が捉えれなければ、スクリーンリーダーなどの支援技術を利用するユーザーに、期待通りのコンテンツが届いていないことに気づける

  4. 円滑なコラボレーションのため
    テストコードは、単純なドキュメントよりも優れた補足情報。
    テストにはひとつずつタイトルが与えられ、どのような機能が提供されているのか?、どのような振る舞いを持つのかが記されている
    →それらのテストをパスしているから、補足内容と実装内容が異なるということもない
    →だからレビュー時間も少なくなるし、新しく入ってきた人の認知コストも下がる

  5. リグレッションを防ぐため
    日常的に実施する自動テストは、リグレッション防止に最適(リファクタリングを行う予定があるから、テストを書くみたいな...)
    →細かくモジュール分割することで、モジュール単体の責務やテストはシンプルになる一方でモジュール同士の依存関係が発生し、依存先の変更によりリグレッションが発生しやすい構造ができてしまう
    →こういったモジュール(UIコンポーネント)同士が連動して提供される機能に関するテストは、結合テストで保証される
    →またUIコンポーネントの場合は、機能とともに見た目も提供している。そのため単体テストを書いてみても、見た目のリグレッションは防げない。そのためこれを防ぐためにビジュアルリグレッションテストを書く

テストを書く障壁

テストコードを書く目的が定まっていても、

  1. テストを書く習慣がなく、どのように書けばよいかわからない
  2. テストを書いてる時間があるなら、機能を追加したい
  3. メンバーのスキルがまばらで、保守運用に自信が持てない

チームで自動テストコードを運用するにあたり、これらの障壁とどのように対峙するべき?

  1. テストをどのように書けばよい?
    新機能追加の際に、これまでにコミットされたコードを参考にしながらガイドラインに従ったコーディングをする
    →これはドキュメントを眺めているよりもずっと、習得速度が速い学習方法
    →テストも同様
    →プロジェクト内に参考になるテストコードがないから、ワイはこの書籍のサンプルコードを参考にする
    上達への近道は、実装例をたくさん見て練習すること。この書籍のコードは現場で書かれているテストコードに近い具体的な実装例で構成されている
    →全部のテスト手法を網羅するのは不可能なので、まずコツだけ掴んでどれだけ書くべきかを関係者間で検討する

  2. テスト書く時間をどう作る?
    テストコードもコミットするとなると、短期的に見て開発スピードは落ちる
    →十分な時間確保のために、素早くテストコードを書けるよう上達するほかないが、誰もがテストコードを素早く書けるとは限らない
    やっぱり最初のうちは、プロジェクトコードとテストコードを同時にコミットするために、それに見合う十分な時間確保=PBIとして計上してもらい、チームのプランニングに反映することが大切
    →「自動テストコードは必要である」という合意をチームで形成する必要がある
    フロントエンドのコードは寿命が短い。と言われることが多い(これはフレームワークやライブラリの変化の速さからくるもの)が、だからといって「作り替えが前提だからテストコードは書かなくてもよい」ということにはならない
    例)UIの刷新
    テストコードを書く習慣があれば、このような大がかなりな変更にも対応可能。刷新中に、機能が壊れたことがあればテストが教えてくれる

  3. テストを書くと時間が節約できる理由
    テストを書くことは、短期的に見ると個人の時間がとられる活動だが、長期的に見るとチームの時間は節約できる
    →自動テストがなければ、手動テストでの動作確認時に「手戻り」が発生し、自動テストを書いていれば気づけた問題に対処するのにかなりの時間を要する
    →さらに長期的にみると、自動テストが書かれていれば起こらなかったリグレッションが運用段階で発生して時間を浪費する

※テストを全員が書くためには?
テストが開発と同時に書かれていない理由として「プロジェクトでこれまで誰も書いてこなかったから」というのもある
→リリースを終え、運用フェーズに入った状態で「あとから書いていく」のは超厳しい。ステークホルダーを巻き込み、マイルストーンを設定し、数人で取り組むような大掛かりなものになりがち
時間の経過とともにテストを書くべき対象もどんどん増えてきて実現難易度は増す一方。

チームにテストを書く文化が根付くか否かは、初期設計段階で決まる。コードが小さいうちに方針を示しておくことで、どのようにコードを書けばよいのか共通認識が生まれるため
→前例をコミットしておけば、テストに不慣れなメンバーでも前例を参考にある程度のテストが書けるようになる

Yusuke InaiYusuke Inai

第2章 テスト手法とテスト戦略

フロントエンドテスト手法は多い
→まずは「範囲」と「目的」の組み合わせを理解して、適切な自動テストをコミットして確かなメリットを感じる必要がある

詳細は以下記事
https://codezine.jp/article/detail/17672

Yusuke InaiYusuke Inai

第3章 はじめての単体テスト

○テストの実行法としては、CLI経由かVSCodeからJest Runnterかの二択が存在

すべてのテストファイル=CLI
特定のテストを実行=Jest Runner

○条件分岐に着目してテストを書くことが基本
テストタイトルは、関数が提供する機能を表すふさわしいものにするべし
→この関数を後から利用するときにも、こういったテストがあると「どういった意図があってこのような処理が施されたか?」がわかりやすい

    test("50 + 50は100", () => {
      expect(add(50, 50)).toBe(100);
    });
    test("70 + 80は100", () => {
      expect(add(70, 80)).toBe(100);
    });
    test("返り値は、第一引数と第二引数の「和」である", () => {
      expect(add(50, 50)).toBe(100);
    });
    test("合計の上限は、'100'である", () => {
      expect(add(70, 80)).toBe(100);
    });

○閾値と例外処理
期待しない入力値が混入した場合に、例外をスローすることでデバッグする際にいち早く問題に気づける
→TSの場合は入力値にある程度型の制限が加えられるため、より詳細な期待値に限ってランタイムで例外をスローする実装が必要
→例外のスローを検証するテストのアサーションも存在

この場合は以下のように、expectの引数は値ではなく「例外発生が想定される関数」を与える

expect(例外スローが想定される関数).toThrow();

https://jestjs.io/ja/docs/expect

○用途別のマッチャー
arrayContainingが微妙にわかりづらかった
https://jestjs.io/ja/docs/expect#expectarraycontainingarray

○非同期処理のテスト

resolvesを使用したアサーションをreturnする方法がおすすめ
イメージは以下。

    test("指定時間待つと、経過時間をもって resolve される", () => {
      return expect(wait(50)).resolves.toBe(50);
    });

また別の方法としてasync / await を用いる方法がある。

    test("指定時間待つと、経過時間をもって resolve される", async () => {
      await expect(wait(50)).resolves.toBe(50);
    });
    test("指定時間待つと、経過時間をもって resolve される", async () => {
      expect(await wait(50)).toBe(50);
    });

async / await を用いると1つのtest関数内に他の非同期処理のアサーションを収めることが可能

※1
expect.assertions
https://jestjs.io/ja/docs/expect#expectassertionsnumber

非同期のコードをテストにおいて、コールバック中のアサーションが実際に呼ばれたことを確認する際にしばしば便利。

※2 非同期関連のテスト注意点
・非同期処理を含むテストは、テスト関数をasync関数で書く
・.resolves や .rejects を含むアサーションはawaitする
・try...catch 文による例外スローを検証する場合、expect.assertionsを書く

Yusuke InaiYusuke Inai

第4章 モック

●モックの必要性
テストは実際の実行環境と同じ状況に近づけることで、より忠実性の高いものになる
→しかし、実行に時間がかかるケースや環境構築が大変なケースに直面することがある
→代表例:Web API経由で取得したデータを扱う場合
ネットワークエラーなどが原因で失敗するケースがあるし、「失敗した場合」のテストを書くことはむずかしい

テストしたい対象はWeb APIそのものではなく「取得したデータに関連する処理」なので、Web APIサーバはテスト実行環境に必ずしも必要ない
→このようなケースで「取得板データの代用品」として登場するのが「モック(テストダブル)」
=実行困難なテストを可能にするだけではんく、効率化のためにモックは重要

●モックモジュールを利用したスタブ
jest.mock() / jest.requireActual()
https://jestjs.io/ja/docs/jest-object#メソッド

実際にモックモジュールを最も利用するシーンは、ライブラリの代用
→例えば、next/router という依存モジュールに対して、next-router-mockという代用実装ライブラリを適用するなど

jest.mock("next/router", () => require("next-router-mock"));

●Web API のモック
Web API に関連するコードは、Web APIクライアントを代用品(スタブ)に置き換えることで、テストが書けるようになる
固定のレスポンスと動的に切り替え可能なレスポンスを扱うテストの2つが存在

☆レスポンスを再現するテスト用データ = フィクスチャー

●モック関数を使ったスパイ
→活躍するのは、テスト対象の引数に「関数」があるとき

export function greet(name: string, callback?: (message: string) => void) {
  callback?.(`Hello! ${name}`);
}

この場合、以下のようなテストを書くことでコールバック関数の実行時引数を検証することが可能

test("モック関数はテスト対象の引数として使用できる", () => {
  const mockFn = jest.fn();
  greet("Jiro", mockFn);
  expect(mockFn).toHaveBeenCalledWith("Hello! Jiro");
});

これは後の「フォームに特定のインタラクションを与えた後、送信される値は〜である」といったことを検証するテストで出てくる。(6章以降)

●Web API の詳細なモック
入力値を検証したうえでレスポンスデータを切り替える、モックの詳細な実装法について解説

ここで話した以外にも、Web APIに依存したテストの書き方は「ネットワークレイヤーでモックする」手法がある。
→ネットワークレイヤーの入力値が検証可能なため、さらに詳細なモックを実装することが可能
(7章)

●現在時刻に依存したテスト
テストタイsh場が現在時刻に依存している場合、特定の時間帯になったらテストが失敗してしまうので脆いテストにつながる
→テスト実行環境の現在時刻を固定して、いつ実行してもテスト結果が同じになるようにする

Yusuke InaiYusuke Inai

第5章 UI コンポーネントテスト

画面に表示する UI コンポーネントとデータの関連に着目してテストを書いていく

※アクセシビリティ
UI コンポーネントテストは、Web アクセシビリティを気にかけるための機会にうってつけ。
マウスを利用するユーザーと支援技術を利用するユーザーの双方が同じ用に要素を識別できるクエリーを使用してテストを書く。
→UI コンポーネントテストは基本機能を検証するだけでなく、アクセシビリティ品質を向上するきっかけとなる

※fireEvent vs userEvent
https://zenn.dev/tnyo43/scraps/6d15ee29867b7e

https://zenn.dev/k_log24/articles/4c1cd37ff0ca50

※ユーティリティ関数を利用したテスト
入力項目が多いフォームでは、関数化の効果が高い

※非同期処理を含む UI コンポーネントテスト
フォームでAPIを叩くまでのテスト
Arrange-Act-Assert(AAA)パターン=「準備、実行、検証」の3ステップにまとめられたテストコード

※スナップショットテスト
対象ファイルのコミット済み.snapファイルと、現時点のスナップショットを比較し、差分がある場合にテストを失敗させることがスナップショットテストの基本。
またインタラクション実施後の snapshot も記録できる
→ UI コンポーネントに与えた Props にもとづく出力結果だけでなく、インタラクション実施後の出力内容を記録させることもできる。

※暗黙のロールとアクセシブルネーム
望ましいマークアップで UI コンポーネントが実施されている場合、暗黙のロールを参照するクエリーでテストコードが書ける
暗黙のロールは要素に与える属性に応じて変化する
アクセシブルネーム

Yusuke InaiYusuke Inai

第6章 テストカバレッジ

カバレッジを上げるためのコツ

  • 呼び出しを通過しているか?
  • 分岐を通過しているか?

実装内部構造を把握して、論理的に書く「ホワイトボックステスト」にカバレッジレポートは欠かせない

カバレッジは定量指標となるため、プロジェクトによっては満たすべき品質基準として据えられることもある
→分岐網羅率が80%以上でなければ、CIはパスしないといったパイプラインを組むことも可能
→注意しなければいけないのは、数値が高いからといって品質の高いテストとは限らないと。テスト実行時に通過したかどうかの判定にとどまるため、バグがないことを証明するものではない

ただテストをもれなく拡充したいというシーンでカバレッジレポートを確認することは大切

レポーターも、テスト環境の整備には大切

Yusuke InaiYusuke Inai

第7章:Webアプリケーション結合テスト

●React Context の結合テスト
具体的にはToast表示のテスト。
→テスト観点としては

  • Provider が保持する状態に応じて表示が切り替わること
  • Provider が保持する更新関数を経由して、状態を更新できること

Context テストの書き方は2種類ある
①テスト用のコンポーネントを用意し、インタラクションを実行する
→こっちはカスタムフックも含めての結合テストになっているため、より広範囲のテストになる
②初期値を注入し、表示確認をする

●Next.js Router の表示結合テスト
Next.js の Router 似関連するテストを書くには、モックを使用する必要がある
→next-router-mock。Jest で Next.js のRouter に関するテストを実施できるようにするモックライブラリ
→Link によるRouter の変化や、useRouter による URL 参照やURL変更の結合テストをjsdom上でも可能にする

●Next.js Router の操作結合テスト
セレクトボックスを操作することによって、Next.js Router を操作して、URLパラメータを書き換える
※初期表示のテスト
→「URLの再現、レンダリング、要素の特定」という処理はすべてのテストで必要
→そこでセットアップ関数として1つの関数にまとめる

※あえて狭い範囲でテストする
ここではUIコンポーネントの操作で起こるURLパラメータの変化についての結合テスト
→「URLパラメータの変化が一気に影響を及ぼすこと」というテストでもよいが、今回のような狭い範囲の検証をテスト観点とすることでUIコンポーネントの責務も、付随するテストコードも目的が明確になる
(今回のUIコンポーネントの責務は、Next.js Router を操作して、URLパラメータを書き換えること)

※セットアップ関数のような共通化
UI コンポーネントのテストは似たようなパターンの事前準備だけにとどまらず、似たようなインタラクションが必要になることが多い。
→テストに向けた「事前準備、レンダリング」だけでなく、操作対象を関数で抽象化することにより、可読性の高いテストコードを書ける

https://kentcdodds.com/blog/avoid-nesting-when-youre-testing

●Form バリデーションテスト
「入力内容に応じて、どのようにバリデーションが実施されるか?」が、この UI コンポーネントのテスト観点。
今回扱うフォームコンポーネントの責務

  • 入力Formの提供
  • 入力内容の検証(バリデーション)
  • バリデーションエラーがあればエラー表示
  • 適正内容で送信を試みたとき、onValid イベントハンドラーが実行される
  • 不適正内容で送信を試みたとき、onInvalidイベントハンドラーが実行される

※セットアップ関数の戻り値には、イベントコールバック検証のためのスパイを含めることも可能

●Web API レスポンスをモックするMSW
※MSW
Web API リクエストをインターセプトして、レスポンスを任意の値に書き換えることが可能
→Web API サーバーが起動していなくてもレスポンスが再現可能なため、結合テストのモックサーバーとして使用することができる

※MSW の利点
テスト単位でレスポンスを切り替えることはもちろん、発生したリクエストのheaders や query の内訳が詳細に検証できる
→また、ブラウザで発生するリクエストとサーバーで発生するリクエストのどちらもインターセプトが可能なため、BFF を含むフロントエンドテストの至る所で活用できる

※Fetch API の polyfill
テスト環境のjsdomにはFetch APIが用意されていない
→そのため、Fetch APIを使用したコードがテスト対象に含まれていた場合はテストに失敗する
→Jest 標準のモック機構でAPIクライアントをモックしている場合はFetch API呼び出しに到達しないため問題にならないが、MSWを使用したネットワークレベルのモックの場合はこの課題に直面する
→その場合は、テスト環境向けにFetch API のpolyfillであるwhatwg-fetchをインストールしてすべてのテストで適用されるようにセットアップファイルでimport しておく!

※エラーレスポンス
テストごとにレスポンスを上書きする方法もあるが、入力内容によってエラーレスポンスを意図的に発生させることも可能
必要なエラーパターンに応じて、リクエストハンドラーを設計する

※まとめ
今回はWeb API のレスポンスに連動して「PostFormコンポーネント、AlertDialogコンポーネント、Toastコンポーネント」が機能するという広範囲に及ぶ結合テスト
→今回はUIコンポーネントのバリデーション機能に対するテストは対象外
→子コンポーネントに委ねた処理までをテストしてしまうと、親コンポーネントの責務が不明瞭になってしまう。親コンポーネントに書かれている連携部分に集中してテストを書くことで、必要なテストが明確になるだけではなく、責務協会がはっきりとした設計になる

●画像アップロードの結合テスト
画像選択のためのモック関数と画像アップロードAPIを呼ぶモック関数を準備することで、ファイルアップロードの検証が可能
→テスト対象のコードの何を検証したいのか?という点に注目して、モック関数を組み合わせることでE2Eまではいかなくとも結合テストでもエラー分岐などを検証することが可能

Yusuke InaiYusuke Inai

第8章:UI コンポーネントエクスプローラー

●Storybook の基本

  • jsdom を利用した単体・結合テスト
  • ブラウザを利用したE2Eテスト

→ Storybook の UI コンポーネントテストはこの2つのテスト区分の中間に位置するテスト手法。

●3 レベル設定のディープマージ
登録するひとつひとつのStoryは「Global / Component / Story」の3レベル設定をディープマージしたものが採用される
→共通で適用したい項目を適切なスコープで設定することで、Storyごとの設定が最小限で済む

●必須アドオン
※Controls を使ったデバッグ
「見た目が意図通りか?」というデバッグは、こういった作業の積み重ねで
ただしこれはインストール時に適用した@storybook/addon-essentialsに含まれている

※Actions を使ったイベントハンドラーの検証
Props 経由で渡されたイベントハンドラーがどのように呼び出されたか?をログ出力する機能が「Actions」で、@storybook/addon-actionsで提供されている
これもインストール時に適用されている

※Actions を使ったイベントハンドラーの検証
※レスポンシブレイアウトに対応するViewport設定

●Context API に依存した Story の登録
Context API に依存したStoryは、Decoratorを活用すると便利
→初期値を注入できるようにProvider を作り込んでおくことでContextが保持する状態に依存したUIを端的に再現可能

※Storybook Decoratorの概要
各Storyのレンダリング関数ラッパー。配列で複数指定可能

●Web API に依存したStoryの登録もできる
MSWを利用すればStorybookをビルドして静的サイトとしてホスティングさせることも可能

リクエストハンドラー高階関数を定義しておくとシンプルになる

●Next.js Router に依存したStoryの登録
UI コンポーネントの中には、特定ページURLでのみ機能するものがある
→storybook-addon-next-router を導入することで、Routerがどういった状況にあるかをStoryごとに設定することが可能

●Play function を利用したインタラクションテスト

●addon-a11y を利用したアクセシビリティテスト
アクセシビリティを向上する施策としてStorybookを活用すると、コンポーネント単位でのアクセシビリティ検証が容易になる
→Storybook を確認しながらコーディングすることで、アクセシビリティ上の懸念点を早期に発見できる
アクセシビリティ検証ツールである「axe」を使用しているため、細かい設定やパラメータはそちらのドキュメントを見る

●Storybook の Test runner
→Storyを実行可能な「テスト」へと変換してくれるもの。
テストに変換されたStoryはJest と Playwrightによって実行される

混みいったインタラクションを与えてテストを書きたい場合、Testing Library + jest-domで書くよりも「目視による確認」ができるため、テストコードを楽に書ける。

●Storyを結合テストとして再利用する
Jest によるテストだけでなくStoryもコミットするとなると運用コストがかかる
→どちらもコミットしつつ運用コストを抑えるアプローチとして「Story を結合テストとして再利用する」

UI コンポーネントのテストは、検証を行う前に「状態の準備」が必要。
その準備はStoryを用意することとほとんど同じ

Story を再利用する=「準備の整ったStoryをテスト対象とする」

※@storybook/test-runner との違い
「テストとStoryの登録を1度に行い、工数を削減しよう」というアプローチは、Test runnerによるPlay functionと似てるけどどちらが良いかは目的次第

Jest で Story を再利用するほうが優れている点

  • モジュールモックやスパイが必要なテストが書ける(Jestのモック関数を使用)
  • 実行速度が早い(ヘッドレスブラウザを利用しない)

Test runnerのほうが優れている点

  • テストファイルを別途用意しなくて良い(工数が少ない)
  • 忠実性が高い(ブラウザを利用するのでCSS指定が再現される)
Yusuke InaiYusuke Inai

9章:ビジュアルリグレッションテスト

●VRT がなぜ必要か?
スタイル変化の検知は難しい
→CSSによるスタイル定義は、積み重ねられたプロパティから算出される。適用されるプロパティは色々な影響を受ける
→そのため「見た目の変化」はブラウザ越しに目視で確認する必要がある
→しかし、すべてのページで影響が及んでいないかどうか?判断するのは至難の業

すでにある定義を変更したり削除することは、意図せぬリグレッションを引き起こす可能性がある
→この対処としては「すでにある定義には触れない」というネガティブなアプローチがあるがこれだといつまで経ってもリファクタリングに取り組めず、不健全。

コンポーネント指向だと共通のスタイル定義を使いまわしがち
→共通 UI のスタイル変更は多くの画面に影響を及ぼすことになり、CSS リファクタリングの難しさは依然として残る

●見た目のリグレッションはスナップショットだと無理?
スナップショットテストは見た目のリグレッションを検知する方法のひとつ
→けどCSSのグローバル指定が存在していた場合、グローバル指定の変化はスナップショットテストでは現れない

CSS Modulesを使用している場合には、CSSの指定内容はスナップショットテストには現れない
→HTML出力結果を比較するスナップショットテストでは、十分ではない

●VRTという選択肢
1番信頼できるのは実際にブラウザにレンダリングして確認すること
→テスト対象をブラウザにレンダリングし「画像キャプチャ」をとる
ある時点からある時点までの「画像キャプチャ」を比較して、その差分をピクセル単位で検出することも可能
=これがVRTというもの

VRTはChromiumなどのブラウザをヘッドレスモードで動作させることで実施される
→ヘッドレスブラウザはE2Eテスティングフレームワークに同包されていることがほとんどで、E2Eフレームワークの標準機能としてVRT機能を持つことが多い

このとき比較するのが「ページ単位のキャプチャ」
→ヘッドレスブラウザで画面をリクエストし、画面への遷移が完了した段階で画面キャプチャを撮る
→すべてのページ画面のキャプチャを撮っておくことで、スタイル変更前後の差分検出が可能

ただこのページ単位の比較は大雑把で、例えば共通 UI で「見出し」の余白を変更したとすると、見出しが画面上部に配置されていた場合に見出しより下はすべて差分検出されてしまう
→もし画面に「見出し以外」の変更が含まれているとその差分を見つけることは困難

上記の課題に対して効果的なのが「UIコンポーネント単位」のVRT
→画像キャプチャがUIコンポーネント単位であった場合、影響の及んだ「中粒度のUIコンポーネント」を検出できる。これによりボタンが配置されている場所から下のエリアであっても、差分を検出することが可能

このVRT基盤を支えるのが、「Storybook」
→小粒度のUIコンポーネント、中粒度のUIコンポーネントをStoryとして登録しておくことで、コンポーネントエクスプローラーの枠を超えて、VRT基盤として活用することが可能

●reg-cliで画像比較をする
VRTフレームワーク=reg-suit
このコア機能が「reg-cli」を利用した画像比較

reg-cliは「比較元と比較先」のディレクトリを指定して、そこに含まれる画像の有無/差分の有無を検出する

reg-cli/reg-suit は、Webブラウザに表示されるこのエクスプローラーを使用して画像の差分を確認していく

●Storycap の導入
Storybook の VRT
→Storycap は Storybook に登録した Story の画像キャプチャを撮るツール。reg-suit を中心としたエコシステム「reg-viz」のうちの1つだが、reg-suitのプラグインとは異なるため別途インストールが必要

Storycap はビルド済みのStorybookのほうがレスポンスが早いため、事前ビルドしておく
→npm run storycap を実行するとビルドした Storybook が静的サイトとして起動して、すべての Story キャプチャがはじまる
→ Storybook の初期サンプルには8つのStoryが含まれている

実際には「CSS修正に伴う意図しない影響」が検出できるようになる
→Story単位でキャプチャを撮るため、Storybookが拡充しているほど効果が期待できる

●reg-suitを導入する
ここまではreg-cliを使用して、ローカル環境でVRTを実施していた。
→ここからはVRTを自動化して、GitHub連携するところまでを解説する
→GitHub 連携すると、リポジトリにpushするたび(ここのタイミングは自由)にトピックブランチのStorybookキャプチャ比較が実施されるので、加えようとする変更でどのような画像差分が発生するのか?自動でレポートを受けることができるようになる

最初にどのプラグインを導入するのか?が質問される
→これらのプラグインは任意のCI環境にreg-suitを導入する便利なプラグイン
→検証結果をプルリクに通知するためのプラグインも存在するため、普段のワークフローにVRTを導入できる

※実運用における閾値設定
→実運用において自動化されたVRTがFlakyテスト(稀に失敗するテスト)になることがある
これはブラウザで複数のレイヤーがコンポジットされるとき、アンチエイリアスをかける処理で差分が検出されてしまうことが原因
こうしたFlakyテストに遭遇した場合、差分検出の閾値を緩めることが検討できる
→thresholdRate(差分が発生したピクセル数の全体に対する比率)やthresholdPixel(差分が発生したピクセルの絶対数)を調整して、安定運用できる閾値を検討する

●VRTを利用した積極的なリファクタリング
迷うのは「導入時期」
→一般的には「リリース前後に導入」

特にレスポンシブレイアウトが含まれるプロジェクトではVRTは生きてくる

プロジェクトリリース直前のリファクタリングにも活用できる
→例えばフロントエンドのNext.jsへのリプレース
グローバルCSS定義→必要かどうか判断しかねるグローバルCSS定義の影響範囲特定が簡易になる
本当に必要なCSSだけ残すというリファクタリングはVRTの基盤が整っているとできる

※Storyコミットの習慣化から始めるVRT
Storyを拡充しておくと、UIコンポーネント単位のVRTがすぐに導入可能
→「必要と思われるものだけを登録しておく」というガイドラインを設けることも考えられるが、Story登録しているほど詳細に検証できるため、日常的にStoryをコミットしておくことをおすすめ
→必要になってから、あるいはリリース直前になってからStoryをコミットするのは時期的に実現不可能と判断される
→Storyははじめからコミットする習慣があったほうがよい

Storybook はVRT目的以外にも、テスト戦略の一環として活用できる
→単体・結合・E2Eとあわせて導入を検討する

Yusuke InaiYusuke Inai

第10章: E2Eテスト

●E2Eテストの概要
フロントエンドにおける「E2Eテスト」はブラウザを使用するため、本物のアプリケーションに近いテストが可能
→ブラウザ固有のAPIを使用したり、画面をまたぐ機能テストに向いている
またE2Eテスティングフレームワークを使用して実施することから、次の2つは区別せずにE2Eテストと呼ばれることがある

  • ブラウザ固有の機能連携を含むUIテスト
  • DBやサブシステム連携を含むE2Eテスト

E2Eテストも「何をテストするのか?」という目的を明確にすることが肝心
→実際のWebアプリケーションはDBサーバーに接続したり、外部ストレージサーバーに接続する
→このシステム全体のアーキテクチャーに近い状況を再現するか否かが1つの分岐点といえる

どういった観点でこれらを選択すべきか?それぞれ見ていく

●ブラウザ固有の機能連携を含むUIテスト
Webアプリケーションは通常、ブラウザ固有の機能連携が必要になる
jsdomでは不十分なテスト対象として、つぎのようなものが存在

  • 複数画面をまたぐ機能
  • 画面サイズから算出するロジック
  • CSSメディクエリーによる表示要素切り替え
  • スクロール位置によるイベント発火
  • Cookie やローカルストレージなどへの保存

Jest + jsdomでモックを使用したテストを書くことも可能だが、テスト対象によってはブラウザを使用した忠実性の高いテストとしたい場合もある
→このとき選択肢としてあがるのがUIテスト
→「ブラウザ固有の機能&インタラクション」に着眼できればよいのでAPIサーバーやサブシステムはモックサーバーを使用し、E2Eテスティングフレームワークで一連の機能連携を検証する

●DBやサブシステム連携を含むE2Eテスト
DBサーバーや外部サブシステムと連携して、以下のような機能を提供する。
このように本物に近い連携を可能な限り再現して行うテストを「E2Eテスト」と呼ぶ
E2EテスティングフレームワークのUIオートメーションで、テスト対象のアプリケーションをブラウザ越しに操作する
→Webフロントエンド層、Webアプリケーション層、永続層が連携することを検証するため、忠実性の高い自動テストと位置づけられている
→トレードオフとして多くのシステムが連携するため、「実行時間が長い、不安定で稀に失敗する」といった弱点もある

Docker Compose によるE2Eテストは、テスト環境の構築と破棄が容易
→CIの単一ジョブで実行できるため、開発ワークフローに組み込み、すぐに自動化することができる

このスクラップは3ヶ月前にクローズされました