#2 テスト自動化の落とし穴:テストの不吉な臭い(再掲載)

xUnit Test Patterns ざっくり解説シリーズ

はじめに

前回は、テスト自動化のゴール(達成・維持したい状態)についてxUnit Test Patternsを使って解説しました。第二回は、テスト自動化の落とし穴であるテストの不吉な臭い(Test Smells)の一部をご紹介します。テストの自動化に取り組んでいるチームでは、大抵不吉な臭いに出会すので、チームで読書会はオススメです。

不吉な臭いとは?

不吉な臭いはもともとは、Kent Beck考案でリファクタリングの本で紹介されました。不吉な臭いは、プロダクションコードや設計に問題にあることに気づくサインで、リファクタリングをするきっかけとなります。

テスト自動化にとりくむ際も、同様にテストコードの現状の問題を察知し、原因を特定し、テストコードのリファクタリングを続けることで、オートメーションで達成や維持したい状態に近づける必要があります。放置した場合は、テスト自動化を続けることがメンテナンスコスト高(High Test Maintenance Cost)となり、せっかくテストを書いていたのに諦めてしまって( Developers Not Writing Tests)、やがてバグ対応に悩まされる日々(Production Bugs)に再び戻ってしまいます。

xUnit Test Patternsでは、次の用に分類しています。その中から、私も繰り返し出会う不吉な臭いを抜粋して紹介します。

Code Smells: テストコード・プロダクションコードの形状を観て気づく危険信号

不明瞭なテスト(Obscure Test):

読んですぐに理解ができないテストコードであれば不吉なサインです。本来はテストコードはユニットやコンポーネントを理解するためのドキュメントの役割を果たすべきです(Test as Documentations)

原因の一つとしては、1つのテストメソッドに複数の観点で準備・実行・検証を記述しすぎている(Eager Test)です。テスト観点を分け記述しても、前提・操作・期待結果の因果関係が読み解けない場合は注意です。

放置すると、テストコード・プロダクションコードの理解を妨げる要因となり、メンテナンスコスト高(High Test Maintenance Cost)を引き起こします。

複雑なロジックがあるテスト(Conditional Test Logic)

プロダクションコードと同様な複雑なWhile if文があるテストコードで、複雑なロジックがあると期待どおり動作するか分からなくなってしまいます。テスト失敗したときに、プロダクションコードではなく複雑なテストコード側にあったとなりがちです。

放置すると、Obscure Testを引き起こします。また、テスト内容の信頼性がさがり、ユニットやコンポーネントの動作の信頼性もさがります。

テストすることが難しいコード(Hard-to-Test Code)

本来は、ユニットやコンポーネントはテストしやすい構造や振る舞いが望ましい(design for testability)ですが、テストが記述できない、テストが難しいプロダクションコードに出会えば、設計に問題を抱えていることを示す不吉なサインです。テスト自動化は設計活動と関連深いことも忘れてはいけません。

原因の一つとしては、ユニットやコンポーネントがUIや環境などと密結合で分離が不十分が考えられます。書籍レガシーコード改善ガイドでは、テストができないシチュエーションとその対策が多数紹介されています。そちらも参考になります。書籍テスト駆動開発でも、副作用ありの関数でテストするのが厄介なコードを値オブジェクトパターンを適応して、テストしやすいコードに変換する様子をチュートリアルで示しています。

放置すると、ユニットやコンポーネントの動作の信頼性を下げます。システムテストのみに頼った動作確認は、問題箇所の特定(Defect Localization)に苦労し、メンテナンスコスト高(High Test Maintenance Cost)を引き起こします。

Code Smellsはそのほか、 テストコードの重複で読む際に手間や修正の際に手間(Test Code Duplication)、や プロダクションコードの中にテストコードが混じっている(Test Logic in Production)が知られています。

Behavior Smells: テスト実行した後に気づく危険なサイン

アサーションルーレット(Assertion Roulette)

テストが失敗した際に、テストランナーが出力した失敗レポートを読んでも何の検証(Assert)で失敗したかが正確に分からない場合は危険信号です。

原因の一つは、1つのテストメソッド内で多数の検証(assert)があるテスト(Eager Test)になっていることです。アサーションルーレットはテストを書くことになれていないと陥りがちな罠で研修やコーチングの際によくフィードバックしています。End2Endのシナリオテストも長くなりがちなので注意が必要です。観点を分けても、テスト失敗時のレポートを読んで理解できない場合は、テストのクラス名やメソッド名やAssertのメッセージの書き方に注意を払う必要があります。

放置すると、テスト失敗の際に、問題箇所の特定と修復に時間がかかり、メンテナンスコスト高(High Test Maintenance Cost)を引き起こします。

結果に一貫性のないテスト(Erratic Test)

同じテストを実行してもテスト結果が成功することも失敗することもあり、実行結果に一貫性に欠ける場合は危険信号です。GoogleのJohn Micco氏による”Flakyなテスト”が有名かもしれません。こちらもテストの実行結果に一貫性がない事象をさします。テストの実行順番が変わった、テストメソッドを一個追加すると以前パスしていたテストが失敗する場合もあります。

原因の一つは、テストメソッドが各々が独立しておらず、テスト間で共有された状態変更(メモリ、データベース、ファイルetc..)によって互いに干渉してしまうです。

放置すると、合格失敗の判定が機械ではなく人が判断する必要があるや、テストが失敗した原因の特定に時間がかかり、メンテナンスコスト高(High Test Maintenance Cost)を引き起こします。Erratic Testは、原因特定が厄介になることが多いです。筆者も何度か悩まされました。

壊れやすいテスト(Fragile Test)

ユニットやコンポーネントといったテスト対象(SUT)をちょっと修正すると、過敏に反応して多くのテストケースが失敗してしまう事象は危険サインです。「Fragile」は配達分野では、日本語だと「割れ物注意」に相当する言葉です。ちょっとしたSUTの変更でテストコードの変更箇所が多くなってしまっているのであれば、変更箇所が少なくなるように工夫が必要です。

原因の1つとして、テストコードからテスト対象のUI要素やAPIに直接依存が多すぎることでインタフェースの変更で多数壊れてしまうです(Interface Sensitivity)。ページオブジェクトパターン、Custom Assertionなど適度に抽象度をあげて、テストコードからテスト対象のインタフェースの直接依存を減らしメンテナンスしやすくする工夫が必要です。筆者の場合は、Mockの誤用によってFragile Testに陥った経験があります。

放置すると、テストの修正コストがかさみ、High Test Maintenance Costを引き起こします。心が折れて、 Developers Not Writing Testsに陥ることも考えられます。

遅いテスト(Slow Test)

テスト結果が出るまでに時間がかかるは不吉な臭いです。テストが終了するまで待っているや気が逸れて別の作業をしている時間長くなってきているなら注意が必要です。

原因の一つとして、ユニットではなくコンポーネントやシステムレベルの自動化に依存しすぎの場合に陥ります。

放置すると、テストを含むインテグレーションに時間がかかりすぎ繰り返し実行すればするほどコストとして跳ね上がります。あるいは、テスト実行されなくなってバグを混入からの修正コスト高を引き起こします。

そのほか、Behavior Smellsとして、 原因特定のために頻繁にデバッカーを立ち上げてテスト実行している(Frequent Debugging)自動化不足でテストのたびに人が何かしらの操作や確認を行っている(Manual Intervention)が知られています。

xUnit Test Patternsではそのほか、Buggy Tests、Developers Not Writing Tests、High Test Maintenance Cost、Production Bugsが紹介されています。くわしくは本を確認してください。

--

--