🔰

テスト初心者がE2Eを導入した話

2024/11/26に公開

はじめに

SpecteeにてSpectee SCR の開発を行なっている関根と申します。主にフロントエンドを担当しています。

半年ほど前、チームにフロントエンドのE2EとバックエンドAPIのE2E、2つの自動テストの導入を行いました。今回はフロントエンドのE2Eにフォーカスして、ゼロから自動テストを導入する際の進め方や戦略について共有したいと思います。どなたかの参考になれば幸いです。

テストの目的を考える

まず自動テストを導入する目的を明確にすることが重要です。
フロントエンドのテスト手法は様々あるので、目的が定まっていないと、適切なテスト手法を選ぶのが難しく、結果的に過不足のあるテストになってしまうことがあります。

自動テスト検討の背景

私たちのチームでは、リリースごとにプロダクトオーナーによる手動テストを実施していました。
数ヶ月に一回のリリースならなんとか手動テストで耐えられるかもしれませんが、リリース頻度が増え、機能が増えると、毎回の手動テストは大きな負担になります。

またリリース時に不具合が見つかると、その修正や検証に時間を割かれ、リリース作業に遅れが出るだけでなく、並行している機能開発にも影響が出ることも体感していました。

そういった背景から、以下の目的を実現するために自動テストを導入することになりました。
・毎リリースで一定の品質を保ち、安全で迅速にリリースできるようにする
・不具合を事前に検知し、修正コストを下げる

目的が明確になったら、次にどのテスト手法を用いるかを検討します。

テストの種類を把握する

Webアプリケーションは、下記①から⑥で表されるように一つの機能を提供するために様々なモジュールを組み合わせて実装されています。

①ライブラリが提供する関数
②ロジックを担う関数
③UI を表現する関数
④Web APIクライアント
⑤APIサーバー
⑥DBサーバー

テストを書く際は、この①〜⑥のうち「どの範囲をカバーするテストか」を意識する必要があります。テストのレベルは以下の4つに分類されます。

  1. 静的解析
    ESLintやTypeScriptなどによる静的解析。 一つ一つのモジュール内部検証だけでなく、②と③の間、③と④の間のように隣接するモジュール間連携の不整合に対して検証ができる
  2. 単体テスト
    ②のみ③のみのように単純なコンポーネントや1つの関数の振る舞いをチェックするテスト。
  3. 結合テスト
    ①から④まで、②から③までのように各コンポーネントや関数、ライブラリを組み合わせた時の振る舞いを確認するテスト
  4. E2Eテスト
    ①から⑥をヘッドレスブラウザ+UIオートメーションで実施する、実際のユーザー操作に即したテスト

上記は「機能テスト」と呼ばれ、開発対象の機能に不具合がないかを検証します。
単体テストと結合テストは、フロントエンドだけで完結します。E2Eテストはバックエンドと連携するため遅く重たいものの、より本番環境に近い状態でテストを行うことができるので忠実性の高いテストといえます。

テスト戦略モデル

下の画像左のようにテストレベルをどのぐらいの分量に収めるのが理想かを表したテストピラミッドというものがあります。下層のテストを多く書くことで、より安定した費用対効果の高いテスト戦略になる、ということが提唱されています。
一方で画像右のように、テストピラミッドとは真逆のアンチパターンとしてアイスクリームコーン型というものがあります。これは、極めて少ない量の自動テストと、大量の手動テストに依存した状態です。

テストピラミッドとアイスクリームコーン引用:https://codezine.jp/article/detail/19909

我々の状況はこのアイスクリームコーン型でした。自動テストがなく、手動テストに依存している状態です。
しかし、いきなりテストピラミッドに近づけるのは難しいため、まずは手動テストで行っていることを自動化するE2Eテストを導入することにしました。

どのようなテストを書くか

手動テストからの脱却として、E2Eテストを書くことに決定しましたが、具体的にはどのようなテストを書いていけばいいのでしょうか?

前述した通り、E2Eテストでは実際のユーザー操作に対する挙動を自動的に検証します。
ただすべての導線を網羅しようとすると、テストが煩雑になり壊れやすくなるリスクがあります。そこでユーザーにとって重要な機能が確実に動作することを検証するように、エンジニア目線の細かいテストよりもビジネス目線でのユースケースを元に要件を満たすテストを書いていくことにしました。

技術選定

具体的にテスト自動化への道筋ができてきたら、技術選定を行います。

私たちのチームではPlaywrightCucumberを採用しています。
Playwrightは、Microsoftから公開されているNode.js ベースの E2E テスト自動化フレームワークです。Cucumberは、ビヘイビア駆動開発(BDD)に基づくテストツールで、テストシナリオを自然言語(Gherkin)で記述します。

当初はローコード自動テストサービスを試験的に使用していましたが、以下の理由からPlaywrightとCucumberの採用に至りました。

管理やレビューのしやすさ

開発メンバーが実装・メンテナンスを行うのでコードベースのPlaywrightの方が馴染みやすいという点や、レビューの観点でも差分の確認などがしやすいというメリットがありました。

仕様書とテストコードの二重メンテナンスを防げる

PlaywrightとCucumberをの組み合わせによって、仕様書とテストコードを一元管理できます。

たとえば、ログインフローのテストを行う場合、featureファイルに以下のようなシナリオを定義します。このfeatureファイルがそのまま仕様書となります。

login.feature
Feature: login

    Scenario: ユーザーはログインできる
        Given ログインページにアクセスする
        When ユーザー情報を入力し、ログインボタンをクリックする
        Then プロダクトのTOP画面が表示される

そして、その仕様書に基づいてテストコードを書きます。

login.steps.ts
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { Page } from 'playwright';

let page: Page;

Given('ログインページにアクセスする', async () => {
  page = await browser.newPage();
  await page.goto('https://example.com/login'); // ログインページのURL
});

When('ユーザー情報を入力し、ログインボタンをクリックする', async () => {
  // ユーザー情報(ユーザー名とパスワード)の入力
  await page.fill('input[name="username"]', 'username'); 
  await page.fill('input[name="password"]', 'password123'); 

  // ログインボタンのクリック
  await page.click('button[type="submit"]');
});

Then('プロダクトのTOP画面が表示される', async () => {
  await expect(page).toHaveURL('https://example.com/top');  // 遷移先URLを確認
  await page.close();  // テスト終了後にページを閉じる
});

featureファイルを作成しない限りテストは実行されないので、仕様の変更があった場合でもテスト実装を通じて自然に仕様書が更新されます。
また、Cucumberは自然言語で書くことができるので開発メンバー以外、特にプロダクトに関わるPOにとっても内容を認識しやすいという点がメリットに感じました。

実行時間が短い

E2EはフロントエンドからDB層までの一気通貫で検証を行うので実行時間が長くなりがちですが、Playwrightは実行速度が速いため、効率的にテストを行うことができます。

クロスブラウザ対応

PlaywrightはChrome以外にもFirefoxやSafariにも対応しています。異なるユーザー環境に合わせて検証を行える点も大きな評価ポイントでした。

課題

E2Eテスト導入の結果、POによる手動テストの負担が軽減しただけでなく、リリース前に不具合を検知できるようになったことで不具合の混入も防ぐことができるようになってきました。

一方で、あくまでハッピーパスのみの実装であることから複雑なロジックを含む機能や複数コンポーネント間でのやりとりなど担保できていないパターンもあるのは課題であると認識してます。

また、機能が増えてきたことでテスト実行時間も長くなってきていると感じます。
今後は結合テストを導入して、結合テストで補える部分を増やすことも視野に入れていきたいと思っています。

Discussion