みなさま初めまして。
株式会社ニジボックスの田原と申します。
現在担当しているプロダクトでテスト戦略を見直す機会があり、その際に実施した調査や知見をこちらの記事にまとめていきたいと思います。
これまで簡単なテストを書く機会はありましたが、設計や戦略の段階から考えるのは初めてなので、温かい目で見守っていただけると幸いです。
きっかけ
現在のプロダクトでは、以下のような課題を感じています。
- 途中参画で情報が不足しており、テスト戦略が不透明
- 手動テストの割合や実施頻度が多い
- 結合テストがほぼ未実施の状態で、観点が不足している
メインのモチベーションは手動テストの割合を削減して自動テストへの移管を行うことです。
ただ、その過程で既存の設計の見直しや方針を定める必要があったので、戦略についても調べることにしました。
手動テストは絶対に悪なのか?
昨今の流れ的に手動テストは悪だと思っていましたが、果たして本当にそうなのかをまず疑ってみることにしました。
手動テストの選択肢が生まれる環境とは?
- アプリケーションの構造が非常にシンプルである
- ユーザーインターフェースが主に静的であり、ユーザーの操作が少ない場合、自動化するメリットはあまりないと判断してもよさそうです。
- 短期的なプロジェクトで、機能追加もなく、テストの実施頻度が低い
- 新しい機能や変更が頻繁に行われないのであれば、恒常的にテストを回す必要がないので、自動テストの導入をコスパ観点で見送る選択肢も生まれそうです。
- ユーザーのフィードバックを直接得ることが重要な場合
- ユーザーのフィードバックを受けて機能を改善していくことが前提である場合、自動テストを構成してしまうとメンテナンスコストが上がるので、手動テストに比重を置くという判断はあり得るかもしれません。
- 早期にマネタイズがしたい、デリバリー最優先のお急ぎプロジェクト
- とにかく最速でリリースする!品質は後回し!テストを書く暇があるなら機能を追加せよ!という案件。
他にもあるのかもしれませんが、パッと思いつくのはこのくらいでした。
自動テストの導入を優先すべきかどうかはコスト面でも評価され、プロジェクトの方針によっては手動テストを採用することもあり得そうだなと認識しました。
その上で、自分達のプロジェクトがこれに当てはまっているのか?を考えていくと、戦略を見直すべきなのかどうかの基準が見えてきます。
テストの分類と範囲
テスト戦略を考えるために、テストの種類についてざっくりと理解しておきます。
- 静的解析
- コードが実行される前に、コードの品質やスタイル、潜在的なエラーを検出します。
- TypeScript、ESLint などのツールがあります。
- コードの一貫性を保ち、バグを未然に防ぐことを目的とします。
- 単体テスト
- 個々の関数やコンポーネントが正しく動作するかを確認します。
- Jest、Mocha、Jasmine などが利用されています。
- リファクタリングや変更を行った際に、各機能が仕様通りに動作することを保証し、安全性を高めます。
- 結合テスト
- 複数のモジュールを組み合わせて、相互作用やデータの流れが正しく機能するかを確認します。
- Cypress、Testing Library などが利用されています。
- モジュール間の統合が正しく行われているかを検証し、全体のビジネスロジックが期待通りであるかを確認します。
- E2Eテスト
- 実際のユーザー操作をシミュレートして、最も広範囲な結合テストを行います。
- Cypress、Selenium、Playwright などのツールがあります。
- ユーザーがアプリケーションを使用する際のシナリオを再現し、すべてのコンポーネントが連携して正しく機能しているかを確認します。
この他にも、パフォーマンステストやアクセシビリティテストなどが目的に応じて実施されます。
現在のプロダクトにおいては「静的解析、単体テスト」は導入済みで、それ以外の観点は手動テストに巻き取られているか、不足している、という状況でした。
テスト戦略モデルについて
テスト戦略についても既にいくつかのモデルがあるので、こちらも参考にしていきます。
テストピラミッド型
テストピラミッド型は、下層のテストに比重を置いた戦略モデルです。
実行時間も高速であり、単体テストなどの細かい機能レベルでテストを行うので、失敗した際のリスクも抑えられます。手動テストのような、失敗する可能性があって時間もかかる、という上層のテストの比重を軽くする費用対効果の高い戦略です。
アイスクリームコーン型
テストピラミッド型に反して、アイスクリームコーン型は上層のテストを多く実行する戦略モデルで、主にアンチパターンとして参照されることが多いです。
上層のテストは運用コストが高い上に失敗するリスクもあり、実行時間も長いです。このようなテストに比重を置いている場合、開発サイクルが鈍化していく要因になり得ます。
テスティングトロフィー型
テスティングトロフィー型は、結合テストに比重を置いた戦略モデルです。
Web上のコンテンツは、実際には機能単体で成立するものは少なく、UI操作によって外部へリクエストを送信したり、複数のモジュールを組み合わせて実現するのが基本的です。
ユーザーの操作を忠実に再現するのであればE2Eテストを行うべきですが、それなりのコストが発生します。
そうすると、結合テストが効果的にもコスト面でも一番バランスが良い、ということになり、テスティングトロフィー型の戦略モデルが推奨されていたりします。
現在のテスト戦略
僕が担当しているプロダクトのテスト戦略を、図にしてみると以下のようになりました。
バルーン型などと勝手に名づけることにします。
手動テストが全体の大部分を占めており、結合テストはありません。
アンチパターンのアイスクリームコーン型に酷似していますが、そこから更にプロセスが欠損しています。
テスト戦略を考える
プロダクトの現状と、テストについて調べた内容から、テスト戦略について考え直していきます。
テスト戦略について調べると、トレンドはテスティングトロフィー型であるという結論に割と早く行き着きます。
テスティングトロフィー型を採用してテスト戦略を組んでいる方々のブログも結構多いですし、戦略の意図的にも納得しやすいものになっています。
ただ、自分は以下の人の記事が最も刺さりました。
https://zenn.dev/ik_takagishi/books/5c6c9fe3a7ad2c
QCDのバランスはプロジェクトによって変わりますが、すべてが高水準であるに越したことはありません。
しかし、実際にはリソースに限りがあり、バランスはトレードオフです。
テストにおいても、すべてを達成できるのであれば戦略について悩む必要はありません。
リソースや技術力に限りがあり、何もかもを実施できないのであれば、優先度を定める必要があります。
そのために「目的」を明確にして、それを達成するためのテストを最優先とする、という戦略を考えていきます。
目的ファーストで考える
テストは目的を達成するための手段であり、目的が明確でなければやる意味はありません。
そして戦略は目的を達成するために存在するので、戦略を達成することは目的ではないのです。
特定のテスト戦略モデルに捉われてテストを量産しても、プロダクトにとってそのテストが本当に有用であるかどうかは、自分達が判断しなければなりません。
テストカバレッジはテストの範囲を測るための有用な指標ですが、カバレッジ率が80%であっても、重要なビジネスロジックがテストされていなければ目的は達成できていないことになります。
手段の目的化を回避し、目的を明確にして戦略を考えることを重点的に意識します。
よって、テスティングトロフィーを目指すのではなく、これまでの戦略モデルを参考とした上で自分達の戦略を考えることにします。
自分達の戦略
現在のプロジェクトのテスト戦略はバルーン型です。
テストの大部分は手動で実施されていますが、これが正しい判断なのかについて考えてみます。
現時点で、僕らが開発するアプリは成長線上の過程にいます。日々新しい機能の追加が行われ、ユーザビリティ向上のための様々な案件が立案されています。
初期段階ではシンプルだったアプリ構成は次第に複雑化しており、機能追加によるデグレ影響を確認する必要性は増加していると判断できます。
また、アプリの正常性を確認するための基礎的なスルーテストは高頻度で発生し、すべて手動です。
これはリファクタ対応などの影響が少ないデプロイであっても必ず実施される工程となり、高コストでリスクのある運用になっています。
特に結合テストは未実施の状態であり、ユーザー操作を起点とするようなテストはすべて手動テストに巻き取られています。この状態のまま機能追加が行われていくといずれバルーンが破裂します。
よって、不足している結合テストの充実化と、E2Eテスト導入による手動テストの比率削減を戦略の主軸として狙うことにします。
結合テストを作成する
目的のためのテストを書くので、その意図は明確にしておく必要があります。
何もかもを実施するリソースは無いので、どの観点を捨てるべきかを考えなければいけません。
テスト設計書の作成
結合テストは現時点でほぼ未実施の状態のため、結合テストとして必要な観点を洗い出します。
まずは既存の状況を把握するため、コンポーネント毎に以下のようなテスト設計書を作成していきます。
設計書の目的は現状の理解と改善、目的の明確化、結合テスト観点の追加、のために行います。
作成する過程で、不足している観点をタスクとして追記しておきます。
優先度の基準
現状のテスト範囲と傾向を理解し、優先度の高いテストの基準を以下のような方針で定めていきます。
- ユーザーが目的を達成するための機能
- 複雑なロジックの機能(分岐処理、条件による状態変化)
- 外部ライブラリに依存している機能
- セキュリティに関する機能
機能の核心ではない観点のテストや、自明なロジックのテストは実装基準から排除して、優先度の高い機能のテストを充実させるようにします。
結合テストの観点
テスト設計書で洗い出した不足観点から、結合テストとして実施すべきものを実装していきます。
特に、現時点では以下のような観点のテストを実施すべきと判断しました。
- データ取得と表示機能
- ページ間のナビゲーション
- イベントトリガーによる処理の連鎖
現状これらの殆どが手動テストで巻き取られているため、結合テストとして自動化していきます。
ページ間のナビゲーションはE2Eの方でやればいいので、データの受け渡しやAPI呼び出しなど、重要な観点がある場合のみチェックするようにします。
結合テストの実施は React Testing Library
や Storybook
の Play Function
を使用することにしました。
このプロダクトではコンポーネントの設計パターンに厳密なルールを設けておらず、単体テストは各コンポーネントにぶら下がっているような状態です。なのでContainer
に結合テスト、Presentational Component
に単体テスト、というような棲み分けを行うことは改修コストが重いので断念しました。
代わりに棲み分けについてはテストファイルを分離して範囲を定めることにします。
- MyComponent.test.js(単体テスト)
- MyComponent.integration.test.js(結合テスト)
結合テストの実装
テストファイルを作成したら、実際にテストを実装していきます。上記で挙げた結合テストの観点に基づいてテスト設計書を定義し、目的や具体的なアサーションを決めていきます。
テストの実装が完了したら、CI/CDパイプラインに組み込むことで、コードがプッシュされるたびに自動で実行されるようにしておきます。
E2Eテストを導入する
手動で行っていたテストを最も忠実に再現できるのはE2Eテストです。
論点はエビデンスで、これまで手動で確認してきたものをシステムに移行する場合、システムが信用できるという相応のエビデンスを示さなければなりません。
自動化するテストケースの基準
デプロイ毎に実施される恒常的なテスト観点をE2Eに置き換えていきます。
既存のテストケースを確認し、どこまでの観点を満たせるのかを確認します。
現状は以下のような傾向のテストケースがあるので、これを網羅していきます。
- 表示確認(ビジュアルリグレッションテスト)
- 疎通確認(ページ遷移)
- 接続確認(データ取得)
E2Eテストの実装
Playwright
を使用して、E2Eテストを実施していきます。
最も懸念したいのが、手動で実施していた頃と比較して情報量が低下することで、可能な限り据え置きのエビデンスが出せるようにしたいなと思いました。
Playwright
を導入してテストケースの一部を自動化し、最初に吐き出したレポートが以下です。
このテストで何が行われていたのかは、実装者であれば解明できますが、エビデンスとしては情報が分かりづらいです。
そこで、テストの具体的な実行手順を以下のようにステップを切り分けて書き出し、レポートを見やすく改良していきます。
test('ヘッダーリンクの遷移テスト', async ({ page }) => {
await test.step('トップページにアクセスする', async () => {
await page.goto('https://example.com');
await expect(page).toHaveTitle('Example Domain');
});
// ...
});
レポートの出力結果は以下です。
既存テストケースの手順ごとにステップを区切って、AAAパターンで記述しています。
多少は分かりやすくなりましたが、やはり視覚的な情報がないとエビデンスとして弱い印象があるので、スクリーンショットも撮っておくことにします。
そのままスクショを撮るとローカルのディレクトリに画像が保管されますが、エビデンスとして提出したいため、レポートにスクショを掲載できるようにします。
以下のようにtestInfo.attach
メソッドを使用して、スクショをレポートに掲載します。
test('ヘッダーリンクの遷移テスト', async ({ page }, testInfo) => {
await test.step('トップページにアクセスする', async () => {
await page.goto('https://example.com');
await expect(page).toHaveTitle('Example Domain');
const topScreenshot = await page.screenshot();
testInfo.attach('トップページにアクセスする
', {
body: topScreenshot,
contentType: 'image/png',
});
});
});
以下のような形式でレポートが出力されます。
あとはステップ毎にスクショを掲載しておけば、手動テスト時代のエビデンスと差異はなく、自動化への懸念は解消できていそうです。
このような形で、既存の手動テストをPlaywright
に移管していきます。
今回はレポートの提出と閲覧が手軽にできることを最優先としたので、Allure Report
などのツールを導入するのは一旦見送りました。Playwright
の標準レポートはHTMLファイル一枚で完結しているので、やり取りがスムーズに行えるメリットがあります。
さいごに
テスト戦略と呼べるほど大それたことは出来ていませんが、目的のためにテストを書く、という理解をしてから負担が一気に軽くなったように感じています。
とにかくテストを充実させなければならない、手動テストは悪である、ということに捉われていて、謎の圧を感じていたのですが、目的に割り切って考えることで余計な制約を排除して考えることができるようになりました。
戦略の組み立てについてはまだ途中なので、今後も改善を繰り返していきたいと思います。