はじめに
本記事はAdvent Calendar 2024、「テスト自動化あるある言いたい by T-DASH」シリーズ1、2日目の記事です。
私はとにかく怠惰な性格で、手作業のテストを基本的に嫌がります。いつも自動化できないかを軸に考えていますが、故に多々失敗もしてきました。
そんな私の経験則が実は「あるある」だったりしないかしらと思い、初めてのアドカレ投稿です。
DAOと依存するモジュールのテスト
PHPでWebアプリのスクラッチ開発をしているときのこと。
ビジネスロジックを実装したサービスクラスでは、DBへクエリを実行するデータアクセスオブジェクト(DAO)の具象クラスを使用していました。
なので、このサービスのテストをPHPUnitでテストする時には、事前にDBへテスト用のデータを登録しておく必要がありました。
実装されたサービスクラスが少ない内は特に問題のないまま、順調に開発が進みます。
開発が進むにつれて...
多様なビジネスロジックを実装した複数のサービスクラスが増えていき、PHPUnitのテストコードも比例して増えていきました。
そして、DB側でもテーブル数とともにテスト用のデータも比例して増えていきました。すると、今までは成功していたテストが失敗するようになったのです。
新たなテストで必要になったテストデータが、既存のテストへ干渉するようになったためです。
全てのテストが成功するようにテストデータを調整することは非常に難しく、今後さらに増えていくテストクラスで、常に既存のテストを意識してテストデータを作ることはさらに難しいことから、結果これらのテストクラスはその後陳腐化し、使われなくなっていきました。
得た知見
テストケースごとに使用したいデータの棲み分けをできる仕掛けが必要だった。或いは、データベースと疎結合にしておく必要があった。
疎結合化するために、テスト対象のモジュールが依存性逆転の原則に従った、尚且つ依存性を注入(Dependency Injection)できるよう設計されてる必要があった。
そうすればテストメソッドごと必要なデータを設定できる且つ、テストケース毎にデータが干渉するようなことも避けられた。
本例では該当しませんが、依存性逆転の原則以外のSOLID原則も満たしているとよりテストコードが書きやすいと思います。
更に欲を言うと、システムがクリーンアーキテクチャを採用していることがもっと望ましいと思います。
ユニットテストにユースケースを組み込む
DB内のデータを加工・整形してファイルサーバ上にファイルを格納するWindowsアプリをC#で実装しているときのこと。
ファイルIOを扱うコンポーネント内に組み込んだ、フォルダの存在有無を検証するメソッドのテストコードを書いている際に、後続の工程でテストするであろう以下の2点もxUnitテストへ盛り込みました。
- ファイルサーバ上のアクセス権のないフォルダを指定した場合、きちんとバリデーションエラーを返すこと
- 同サーバ上のアクセス権のあるフォルダを指定した場合は、バリデーションエラーとはならないこと
このテストはよりアプリの本番運用で想定されるユースケースに近く、早い段階でそのシチュエーションの動作を保証できることで手戻りリスクを避けられると考えていました。
このテストは当初期待通りに動作し、アプリは問題なくリリースされました。
月日が経って...
諸般の事情により、ファイルサーバの運用が廃止されることとなり、アプリのファイル出力先はローカルマシン上のフォルダになりました。
無論、ファイルの出力先が変わっただけなのでアプリの改修は不要でした。
しかし、前述した「アクセス権の有無」に関わるユニットテストコードは何と廃止されるファイルサーバ上のフォルダを参照していたために、ファイルサーバのサービス終了とともに自動テストが失敗するようになってしまいました。
結果、プログラムに何の変更もないにも関わらずテストコードを修正(削除)する工数が発生してしまいました。
得た知見
テスト対象のモジュールだけでなく、テストコードもきちんと疎結合となるよう実装すること。
過度な依存関係を持ち込むと外部要因に起因して予期せぬ影響が波及してくる恐れがある。
欲張ったE2E自動テスト
某ローコード開発プラットフォームで実装したWebアプリを開発した時のこと。
そのプラットフォームではSelenium RCによるE2Eテストをサポートしていました。一方で、内部のモジュールなどはプラットフォームにより隠蔽化されていたため、私はビジネスロジックに関わる動作も全てSelenium RCによる自動テストを作り込みました。
ビジネスロジック内に存在するIF分岐も、データ内部の状態遷移も、限界値テストも、画面とDB上に表現されるあらゆる項目を全てです。
勿論、内部動作をきっちり保証するために、テストケース数を減らせる同値分割法なども使用せず、厳密にテストを作り込みました。
E2Eテストでホワイトボックスに近いテストを実装し、C1カバレッジを100%にしたのです。
月日が経って...
しばらくはとても順調でした。軽微な改修もいくつかこなし、デグレが全くないことを保証し続けることができました。
しかしある日、画面表示項目に対して多量の項目削除および項目追加を要求する案件が降ってきました。それも、根本のビジネスロジックに対しては全く変更のかからない、とても単純な案件です。
案件の内容だけを鑑みれば、個々のテストコード修正はとても単純です。ただ追加された項目の検証を増やし、廃止された項目の検証を削除すれば良いだけです。
ですが、その修正が必要となるテストコードの量があまりにも膨大すぎました。
単純な要件の案件で、しかもローコード開発プラットフォームを使っているのに、大量のコードを修正するほどの納期など勿論認められません。
私は泣く泣く、かつては完璧だったテストコードを破棄せざるを得なくなりました。
得た知見
手動で実装するE2Eテストは過度な実装をしないこと。
たった一度使うためだけの自動テスト
最後は失敗談というよりも、自動化して良かったと言える話です。
Windows Server上で動作するPowershellのバッチ処理用プログラムで2つの問題が発生していました。
- 5%前後の確率で予期せぬエラーが発生すること
- 1%弱の確率で、正常終了しているのに処理結果が不正となる現象が発生すること
対象のプログラムはPesterでユニットテストを実装していたのですが、そこでは前者の現象が生じることを既に察知しており、バッチ処理フロー側で自動的なリカバリ機能を備えていました。(予期せぬエラーの根本原因までは追求できていなかった)
一方、後者の問題に関しては自動テストでも全く発生することがありませんでした。
苦労を重ねて調査を進めた結果、何とか後者の現象に対する回避策のようなものを見つけることができましたが、その策が本当に有効なのかどうかを検証するためには、ざっと計算した結果「問題となっているプログラムを含む一連のバッチ処理フローを3,000回処理を実行し、一度も後者のエラーが発生しないこと」で妥当性を評価できると結論づけられました。
さすがに3,000回もの実行を人の手で行うことは、現実的ではありません。
また、バッチ処理フロー全体を通じてエラーが起こらないことを保証しなければならないため、Pesterも使用できません。
そこで私は、本来はETLツールとして位置付けられているソフトウェア「Asteria Warp」を使うことで、バッチ処理フローの実行と処理結果の検証を行うテストを実現し、バッチ処理性能を鑑みて5分周期で自動実行するよう設定しました。
この「Asteria Warpによる自動テスト」では、前述した2つの現象のどちらかが発生したら即時メールを配信するよう作り込んだので、以降の私の仕事は10時間の間メールが来ないことを祈りながら、コーヒーを片手にただひたすら待ち続けるだけになりました。
結果として、エラーは全く発生することなく無事に3,000回の処理は完遂され対策の効果を立証することができました。
このために作った「Asteria Warp」のテスト用プログラムは、二度と使われることはないでしょうが、費用対効果の観点で言えば大きく貢献したと言えるでしょう。
ちなみに、対策前のプログラムを同じ「Asteria Warp」のテスト用プログラムで稼働させたところ、1時間程度でに2つの問題がきちんと再現され、私にメールが飛んできました。
まとめ
以上の失敗談踏まえ、自動テストを作り込み、継続してメンテナンスしていくハードルは決して低くないと考えます。
- ユニットテストが複雑にならないよう、SOLID原則を満たしたプログラムを作る必要がある
- ユニットテスト自体にも依存関係を持ち込まないよう、テストコードの実装でも気を配らなければならない落とし穴はある
- それらを踏まえると、開発者自身にも一定のスキルがないと自動テストを維持保守していくことは難しいと考えられる
- テストのスコープは、ユニットテストからE2Eテストに至るまで、何をどこまで見るかをテストアーキテクチャごとの特性、システムの特性を含めて評価する必要がある
- E2Eテストを最も手厚く実装するアプローチは微妙
テストピラミッドやテストトロフィーといった考え方はあるが、E2Eテストに最もコストを割くべしといった論調はほとんど見ないため
しかし、最後の例でもあるように自動テストは費用対効果として有効だと判断される場合は、たとえ局所的であっても積極的に自動化していくべきだと考えています。
QAエンジニアやSET(Software Engineer in Test)といった役割がいない中で自動テストを導入する場合は、自身が先陣を切って推進していく上に、大きな泥団子化を阻止すべく、後ろに控える開発者たちにも自動テストの利点や維持するための戦略などを理解してもらう必要があるので、大変な苦労を伴うと思います。
それでも、似たようなリグレッションテストを保守開発のたびに手動で行い、それらテストをすり抜けるデグレは起こりうること、そしてその手のデグレが起こるたびにまた改修とリグレッションテストを繰り返す苦痛に比べれば些細な苦労です。
たとえ失敗を繰り返そうとも、その失敗を財産として、より洗練されたプロダクトコードとテストコードを書いていきましょう!