🦔

フロントエンド開発におけるカバレッジ向上施策

2023/06/19に公開

はじめに

皆さんはテストコード書いてますか?
フロントエンドのテストというと、テーブルの合計値表示の関数や手製フォーマッタのパターンチェックなど簡易なところがイメージとしては強いと思います。

今回はレグレッションテストの観点からカバレッジ向上への働きかけを考えてみたいと思います。

レグレッションテストとは?

レグレッションテストは、主にバグの修正、システム開発でコード変更などがおこなわれた場合に実施し、どこにも影響が及んでいないかを調べます。システムはさまざまな機能が組み合わさっているため、一部を変えただけでも、関係している別の部分に影響が及ぶケースもあり、注意が必要です。

上がググったままの内容です。要は改修前後の実行差分を測って、改修による考慮が抜けていた箇所を探し当てるのが本テストの目的となります。

テストカバレッジについて

よくテスト実装の指標としてカバレッジが用いられますが、これはコードの網羅性を示す指標として使われます。例えばコードの改修都度にレグレッションテストをおこなうとして、カバレッジが高いほど実行差分を多く検証できるわけです。

カバレッジをあげる

ここからはJestを使ってカバレッジを改善していく手法について記載していきます。
JestはJavascriptにおけるテスティングフレームワークです。
https://www.npmjs.com/package/jest

カバレッジ取得方法

まずはこちらでカバレッジを取得する方法を確認しておきましょう。
以下のようなコマンドでカバレッジを確認できます。

jest --coverage

カバレッジのレポートはコンソールに出力されるものとは別に、レポートを残すことができます。


htmlで出力されるので、そのままホスティングしても扱えます。

不要なファイルはignoreしておく

以下のようなファイルはカバレッジを誤認させる可能性があるので外しておくと良いでしょう。

  • typescriptの型ファイル
  • モック管理ファイル
  • 定数管理ファイル

型ファイル・モックファイルについてはコード網羅すると必ず通過するので、実践より高いカバレッジが出てしまう可能性があります。 jest.configにて coveragePathIgnorePatterns を設定することで簡単に除外できるので、下準備として対応しておきましょう。

https://jestjs.io/docs/configuration#coveragepathignorepatterns-arraystring

[テスト1] 手続きの単体テスト

具体例として、以下のような処理を切り出しておくのがよいでしょう。

  • 計算処理
  • フォーマッタ
  • 画面サイズ取得
  • etc.

入出力が明確な手続きはアサーションも設定しやすく、期待値を先にコーディングするテスト駆動での開発にも向いています

UIから複雑な処理を切り離しておく

htmlを出力するコンポーネントや、APIデータを取り扱うページ層に計算ロジック等が含まれているのはテストを困難にします。開発段階でテストを意識するのであれば、この手のロジックは関数として切り出しておくのが良いかと思います。
ライブラリを利用していても、インターフェイスを集約したり、テストカバレッジをあげるために切り離しておくのは有益な戦略です。

ブラウザ情報の取得も関数化しておく

Jestを利用する際、windowやdocumentといったブラウザからのパラメータはモック化が必要になります。そのため事前に関数化しておくとテストが楽になります。また、これらの関数は切り離された際に(モック値を返すだけならば)テストをおこなうメリットが下がります。処理を切り離すと合わせて、場合によってはカバレッジの考慮から外してしまうのも良いかと思います。

coveragePathIgnorePatterns というオプションを利用することでカバレッジの対象外とできますが、除外する場合は意図が明確で、プロジェクト内で共通認識されていることが重要です。
https://jestjs.io/docs/configuration#coveragepathignorepatterns-arraystring

[テスト2] UIの単体テスト(スナップショット)

Jestには修正前後で出力差分が無いことを確認するためのスナップショットテストが用意されています。
https://jestjs.io/ja/docs/snapshot-testing

アサーションを利用して、ページタイトルや内容に含まれる文言をヒットさせる方法もありますが、これだとUIごとの評価基準が均一になりません。スナップショットを使えば、生成後のページ全体を記録することができ、CSSや細かな出力の崩れも見逃すことがなくなります。

Snapshotの内容は純粋なhtmlとは違うものの、おおよそ可読性のあるものとなっています。

Storyshotsを使う

React等でコンポーネントを作成し、それをショーケースに一覧できるライブラリ「Storybook」があります。こちらのAddonである「Storyshots」を使うと、コンポーネント毎のスナップショットを簡単に取得することができます。
https://storybook.js.org/addons/@storybook/addon-storyshots

スナップショットは範囲を絞り込んで小さく取得するのがベストプラクティスとされておりますが、コンポーネント単位でのスナップショットは影響範囲の確認でも都合がよく、Storyshotsを用いたスナップショットテストの実施はレグレッションをおこなう上で視認性・管理効率ともに最も優れていると感じています。

[テスト3] インタラクションテスト

ロジックの切り出しやUIコンポーネントごとの単体テストによって、おそらく70%程度のカバレッジが出ているかと思います。コードの網羅詳細は最初に紹介したカバレッジレポートでも確認することができます。

残すところはイベントハンドラー

おそらく、残っているのは下記のケースのようなアクション発火が必要な処理ではないかと思います。

Storybookの Play functionを利用する

アクションイベントをテスト過程で発火できるのがStorybookの「Play function」になります。
https://storybook.js.org/docs/react/writing-stories/play-function

これによって、ボタン押下等のイベント実行の追加が可能となり、より網羅的にテストを実施できるようになりました。

export const Button: StoryObj<typeof Component> = {
  args: {
    label: 'OK',
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await userEvent.click(canvas.getByRole('button', { name: 'OK' }));  },
};

上記はコンポーネントのStoryにボタンクリック操作の実行を追加した例です。

エレメント取得方法を考慮する

ボタンクリック等を実行させるため、UIコンポーネントを掴むためのヒンティングが重要になります。
手段としては以下のようなものが検討できるかと思います。

  • data-testIdを埋め込む
  • roleとラベルで取得する

data-testId を用いる例示は多く出てきますが、個人的には後者だけで完結できればと思っています。<button />タグ等であれば、デフォルトで role="button" が割り当てられ、ラベルとの組み合わせでおおよそ正しいボタンをつかめます。

await userEvent.click(canvas.getByRole('button', { name: 'OK' }));  },

上記の「Play function」の例示でもロールとボタンのラベルで要素取得を試みています。

カバレッジ率だけにとらわれない

インタラクションテストまで完結すると、8〜9割のカバレッジ実現が容易になります。
ただし、それが90%の要求達成を保証するかというと、そういう指標ではありません。

カバレッジはあくまで状況把握の指標です。
前回のテストと比較の結果、8〜9割のコードから差異のない(ある)箇所を検出できている、というのがより正確な認識であり、差異の妥当性についてはE2Eテストなどによって改めて検証していく必要があります。

新たな機能追加をおこなえば、その箇所はユーザー体験を交えてE2Eテストされるべきであり、カバレッジに含めることで検証が不要になるわけではありません。

また、コスト観点から考えても100%を目指すべきではない、という見解がすでに述べられてました。
https://qiita.com/odekekepeanuts/items/d02eb38e790b93f44728

上の記事を参考にさせていただきました。

適切なカバレッジの維持と網羅の把握によって安全なプロダクト運用を目指しましょう。

Discussion