ミューテーションテストの技法でテストの十分性を評価する
これは豆蔵デベロッパーサイトアドベントカレンダー2024第3日目の記事です。
以前の記事でミューテーションテストについて軽く紹介しましたが、具体的な手法については紹介できていませんでした。そこで今回は以前の記事で利用してきた「よくあるサンプルアプリ」を題材にして[1]Javaの開発プロジェクトにミューテーションテストを組み込んでみます。
Javaでのミューテーションテストツールは古くはμJavaのような研究色の強いものもありましたが、最近の動向としてはPITまたはPiTest[2]として知られるツールが活発なようなのでこれを選択します。
この記事のコードサンプルは、Gitlab リポジトリ にありますので、興味がある方はあわせてご利用下さい。
開発の想定シナリオ
#別記事の「よくあるサンプルアプリ」ですが、リリースの承認を取りにいったところ品質管理部門から待ったがかかりました。曰く「そんなフレイキーな事象が発覚したのならばテストの十分性についてはキッチリとした評価が必要ではないか」とのこと。
そこで、実装が誤っていたらしっかりと検出できるテストであることを評価してテストの頑健さを示すことにしました。いわゆる「ミューテーションカバレッジ」を使った頑健性の定量評価です。
ミューテーションテストとは
#ミューテーションテストはテスト対象を評価するというより、実施されているテストが妥当なものであるかを評価するアプローチです。
よくサンプルプログラムで扱われる「FizzBuzz判定[3]」をテストすることを考えてみてください。これをテストする場合、どこまでのパターンを確認すれば十分と言えるでしょうか。
「どのような前提を置いても問題なく完全である」と言い切ろうとしたらとり得る全入力パターンを確認する必要がありますが、もちろんそんなことは現実的ではありません。そこで、「3,6,9・・・といった3の倍数を代表して6でFizzが確認できたから他については問題ないものと考える」ような理論的なアプローチ[4]をすることが多いかと思います。
ミューテーションテストは「作成したプログラムが誤った動作をしていたとしたらテストで検出できていたか」を評価することでテストが十分であるかを評価します。例えば「3で割るべきところを誤って2で割って割り切れる場合にFizzを返すようになっていた」場合、「6に対しては正しくFizzになるが9はFizzにならない」ためテストとしては不十分な可能性がある[5]ことになります。
「プログラムの誤った動作」を起こすためにツールを用いて機械的にプログラムの動作を改変し、改変した状態でテストを行った場合にテストが失敗するのであればそのテストは改変した誤りがあった場合に検出できていたものと評価できます。下記イメージ図(※)のように様々な改変に対してテストを行うことで、テスト不十分な可能性がある誤り(改変)パターンの情報を得ることができます。
※:お分かりだと思いますが、あくまでもイメージ図ですので図中に記載のコードは偽コードです。正しく動作するものではありません
PIT(PiTest)の導入
#PiTestは単独実行可能なコマンドラインツールもありますが、Ant/Maven/Gradleの各ビルドツールに対してはプラグイン機能[6]の提供もされています。
開発している「よくあるサンプルアプリ」はビルドツールにGradleを利用していますので、このプラグインを利用することにします。基本的な設定はプラグインが担ってくれますが、テスティングフレームワークにJUnit5を利用している場合はGradleのpitestタスクにJUnit5を利用する設定が必要です。
細かい条件設定をしないのであれば、build.gradleに以下のようにプラグインの設定を追加するだけでPiTestが実行可能になります。
plugins {
id 'java-library'
・・・
// Gradleの設定にPiTestプラグインを追加
id 'info.solidsoft.pitest' version '1.15.0'
}
pitest {
// JUnit5を利用している場合、JUnit5のテストコードに対しミューテーションテストを実施するためpitestタスクにJUnit5プラグインを指定
junit5PluginVersion = '1.1.2' //or 0.15 for PIT <1.9.0
// 必要に応じ設定を追加
}
GradleプラグインであればGradleのタスクにpitest
が追加され、このタスクを実行することで設定に従ったミューテーションテストが実行されGradleのビルド出力先以下(build/reports/pitest)に結果レポートが出力されます。
PiTestは著名なモックフレームワークとの併用に問題がないことを謡っていますし、少なくともこのサンプルアプリで利用している範疇においてはAllure、Pactなどのテストツールとの併用も可能なようです。
結果評価とテストの補強
#レポートにはミューテーションテストのサマリ情報が提示され、テスト対象のパッケージ/クラスごとにまとめられた情報からドリルダウンしていくことでミューテーションテストの詳細を確認することができます。
サマリ提示情報:
- Line Coverage : テスト対象の条件分岐などをどの程度テストでカバーしたか(通常の行カバレッジ)
- Mutation Coverage : ツールが加えた改変(ミューテーション)に対してどの程度「テスト失敗」として検出=KILLできたか
- Test Strength : 実行されたテストがどの程度ミューテーションを検出する能力を有しているか だと推測される[7]
詳細情報:
- 薄い緑/赤で色付けされたコード : テストでカバーされた/されない行(通常の行カバレッジ)
- 濃い緑/赤で色付けされたコード : ミューテーションを検出(KILL)できた/できない行
- ミューテーションの詳細情報 : ミューテーション箇所ごとの改変内容と結果
例えば上記の結果では、26行目の条件分岐if (4 <= hour && hour < 11)
に対して2種類の境界値変更(4 < hour
やhour <= 11
)と判定の逆転(4 > hour
やhour >= 11
)を行ったミュータントの3/4が検出できていないことが分かります。つまり、ここに関連するテストは「誤りがあったとしても検出できなかった」不十分なテストであることが示唆されます。
対応するテストコードを確認すると、確かに通常の挨拶1ケースしかテストされていないことが確認できます。この状態では今後何か間違いがあった際にテストで検出し損なう可能性がありますので条件の境界近傍でテストを補強したほうが良さそうです。
これらミューテーションに対してテストを失敗させるために条件の境界値に着目してテストを補強していくとミューテーションカバレッジが向上します。奇しくもこの例は「同値分割」や「境界値分析」が不十分な箇所を洗い出して改善させる結果になりました。これで品質部門にも根拠をつけて「テストは妥当です!!」と言いやすくなったのではないでしょうか。
まとめ
#ミューテーションテストの概念とテストツールPIT(PiTest)の簡単な利用方法について簡単に紹介しました。軽めの導入コストにかかわらずテストの質に関する情報を与えてくれる。可能性に溢れる技術ではないでしょうか。
開発への有効な組み込み方などはまた改めて紹介できればと思います。
はい、ゴメンナサイ。当初想定ではこの記事の後に書くつもりだったのですが、多忙にかまけてすっかり忘れたまま別記事で「ミューテーションテスト便利だよね」みたいなことを語る羽目になっていました。いつまでも放置せずにちゃんと回収しておきたいと思います。 ↩︎
発声すると「なんでJavaなのにそれを使った?」と訝しがられること間違いない命名に思えますが、PyTestとは偶然似たような名前になっただけで関係は無いもののようです。JUnitのテストを並列(Parrarel)で独立して(isolated)実行することを目的としたプロジェクト(Pararrel isolated Test)から生まれ、並列実行が必要となる(ミューテーションテストなど)他の技術に展開されたという経緯での命名だそうです。略語命名なので「ピ(ッ)テスト」と読むのが正しい可能性も否定はできませんね。 ↩︎
念のためですが、数値の入力に対して 3で割り切れる場合はFizz / 5で割り切れる場合はBuzz / 3でも5でも割り切れる場合はFizzBuzz / それ以外は入力数値 を返すような判定プログラムを想定しています。FizzBuzzとは英語圏で暇潰しにやる言葉遊びのようなものということですが、筆者はやったことが無いのでこの遊びが面白いかどうかは分かりません。 ↩︎
いわゆる「テストが妥当に設計」されたことをもってテストが十分であることを示すアプローチですね。個人的には大好きなやり方ですが、妥当性をもって評価するため相手にきちんと設計を理解してもらうところにハードルを感じることもあります。(そこの議論がテストの質を向上させてくれるので良いことではありますが、、、) ↩︎
「じゃあこれを考慮して6と9でテストしないと」ということではありませんのでご注意ください。テスト全体で見たときに「どこかに誤りが入ったら検出可能なテスト」であるかを評価することでテストが妥当であるかを評価できるという考え方です。 ↩︎
公式ドキュメントを参考にする限り、Gradleのプラグインのみ3rd Party提供のもののようです。最近の更新頻度が鈍めなのは若干気がかりではありますが、pitest側にもあまり大きく変更が入っていない模様ですので、利用にあたって大きな問題は無さそうです。また、機能拡張を加えた有償版のarcmutateもあるようですが、ひとまずここでは置いておきます。 ↩︎
筆者が調べた限りでは公式ドキュメントなどで正確な定義は見つけられませんでしたが、結果を解析する限り 「テストでKILLできたミューテーション数」 / 「テストされたミューテーション数」 が提示されているようです。Mutation Coverageとは母数が「作成されたすべてのミューテーション」=そもそも該当ミューテーション箇所がテストでカバーされていないケースを含む かどうかの違いがあり、「実行されたテスト」の評価という色合いがあるのではないかと推測しています。 ↩︎