アンチパターンとは、問題の解決を意図しながらも、しばしば他の問題を生じさせてしまうような技法のことです。
本記事では、テストコードを書く開発者が犯しがちなアンチパターンの一つに名前をつけ、その対処法の解説を試みます。
目的:複雑な戻り値が意図せず変わっていないことを保証したい
メソッドの戻り値が複雑な場合、戻り値に含まれる多くの仕様をまとめてテストで保護したいと考えるのは自然なことです。
しかし、完全一致での比較が唯一の解決策ではないのです。
アンチパターン:とりあえず完全一致で比較する
偽陽性によりテストコードの信頼性が低下する
戻り値を単純な完全一致で比較すると、実際にはバグは存在していないにもかかわらずテストケースが失敗する「偽陽性」が発生しやすくなり、テストコードの信頼性が低下します。
たとえば下記のようなテストコードは、典型的なアンチパターンです。
assertThat(it.getWelcomeHtml(), is("<h1>ようこそ!</h1><p>あなたに会えて嬉しいです。</p >"));
</p > 末尾の半角スペースが削除された場合、HTMLを解釈するページ上では何も変化は生じません。しかし、テストケースは失敗扱いとなってしまい、修正の必要性が発生してしまいます。
また、この程度の文字列の長さであれば、期待値と実際の結果の差分箇所は見つけやすいですが、文字列が長くなればなるほど、差分を発見するのにも一苦労となります。
テストケースの信頼性が低下してしまうと、開発者はテスト結果に疑念を抱くようになります。これでは、自動テストの価値である保守しやすく変化に強いソフトウェアを支える柱としての機能が果たせなくなります。
仕様が明確にならない
テストコードは、コンピュータによって実行することで動作を検証するためのものだけではなく、開発者が仕様を理解するために読むドキュメントとしての役割もあります。
戻り値全体を完全一致で比較すると、どの部分が仕様に関する要件であるかが明確にされづらくなります。結果的に、注目すべき特徴が隠れてしまい、テストコードを読んでも実際の動きを理解する助けになりません。
アンチパターンの見つけ方
テスト対象のメソッドが返す文字列や構造体が大きく、目視による確認が困難なケースについて、完全一致での比較をしているケースが、アンチパターンの兆候です。
特に、HTML, XML, JSON, YAMLなどの構造化された形式を文字列表現(String)として返したうえで、その返り値を文字列として完全一致で比較していないか、確認してみましょう。
アンチパターンを用いても良い場合
テストコードの実行結果を観察して仕様を明らかにするような過程では、一時的に完全一致で比較するテストコードを書くことも有効でしょう。
しかし、仕様がある程度明らかになった段階で、発見された仕様に沿ったアサーションに変更すべきだと考えます。
解決策: 完全一致以外のMatcherを利用する
Matcherとは、テストの条件をより柔軟に記述するためのオブジェクトです。
たとえば、JavaのHamcrestでは多数のMatcherが用意されています。
文字列がJSONやXMLなどの構造化された形式に相当する場合は、それらの形式に変換したうえで、構造を比較するようなMatcherもあります。これらを利用するのがよいでしょう。
| Matcher | 説明 |
|---|---|
| StringContains#containsString | 検査対象のどこかに指定の文字列が含まれているかを判定 |
| StringContainsInOrder#stringContainsInOrder | 検査対象の文字列に指定の部分文字列が順番通りに含まれているかを判定 |
| SameJSONAs#sameJSONAs | 検査対象の文字列をJSONとして処理した上で、一致しているかを判定(参考) |
| CompareMatcher#isIdenticalTo | 検査対象の文字列をXMLとして処理した上で、一致しているかを判定 |
先ほどの例だと、たとえば下記のようなテストコードに変更するのが有効かもしれません。
assertThat(it.getWelcomeHtml(), stringContainsInOrder("ようこそ!", "あなたに会えて嬉しいです。"));
おわりに
本記事は、書籍:SQLアンチパターン「第3章:ID リクワイアド(とりあえずID)」に触発されて執筆しました。