うさぎ組

ソフトウェア開発、チームによる製品開発、アジャイル、ソフトウェアテスト

テストをテストする方法-ミューテーションテスト- #gadvent

はじめに

これはG* Advent Calendarの12日目の記事です。今日はミューテーションテストについて書きます。明日はid:nobusue さんです。

概要

PITというツールの紹介です。「Javaプロダクトコードを機械的に変更してからテストを実行したときに、テストはそれを検知できるのか?」ということを調べてくれるツールで、SpockのテストやGradleからの実行に対応しています。

ミューテーションテスト

ミューテーションテストとはざっくりと言えば「プロダクトコードを変更したなら、その振る舞いも変わるはず。テストはその変更された振る舞いを網羅できているかを調べる」というテストです。

対象規模が小さければ手動で毎回やってもいいわけですけど、ツール化されていると楽なことこの上ないです。ということで、今回はJavaプロダクトコードをミューテートするライブラリであるPITについて紹介します。 Groovyアドベントなのは、「GradleとSpockに対応しているから」という理由くらいです。(無理矢理だ!

コードカバレッジよりミューテーションテストを信じるべき理由

上記の説明だけではコードカバレッジでも一緒じゃないの?結局どの程度テストされているかなんでしょ?と思われるかもしれませんが、コードベースに対するカバレッジを使うのであれば、ミューテーションテストのほうがよいでしょう。理由は「C1カバレッジでは境界値はテストできない。同値クラス内のいずれかがテストされていれば十分だからだ。ミューテーションテストは限定的だが境界値をテストできる。例えば>を>=に変更してテストが失敗するかを見ているからだ。」というところです。

PIT

PITのこちらにどのようなミューテートをするかが載っています。→ Mutation operators

デフォルトで無効になっているミューテートもあります。

PITを選んだのは「現在も活発に開発が継続されている」「Gradle, Spring-Bootに対応している」とかその辺です。 他にもすばらしい点は多々あるのですが、どうだ!すばらしいだろ!って説明出来るだけのスキルがありません。さーせん。

サンプルリポジトリ

kyon-mm / pit-example — Bitbucket

使い方

build.gradleに次のように書き足します。あとはgradle pitestと実行したら、ミューテーションテストが実行されて、HTMLのレポートが出力されます。

plugins {
  id "info.solidsoft.pitest" version "1.1.1"
}

// 他の設定をする(ここでは割愛)

pitest {
    targetClasses = ['org.kyonmm.*'] // ミューテーション対象パッケージの指定
    pitestVersion = "1.1.2" // pitestのバージョン指定
    threads = 4 // 実行スレッド数指定
    outputFormats = ['XML', 'HTML'] // レポートファイルの指定
}

例はFizzBuzzをテストしています。コンストラクタ以外はミューテーションのカバレッジが100%になっています。

コンソール

☁  pit-example  gradle pitest
Parallel execution is an incubating feature.
:compileJava UP-TO-DATE
:compileGroovy UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:compileTestGroovy
:processTestResources UP-TO-DATE
:testClasses
:pitest
7:54:58 PIT >> INFO : Verbose logging is disabled. If you encounter an problem please enable it before reporting an issue.
7:54:58 PIT >> INFO : SLAVE : objc[45721]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/bin/java and /Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be 
7:54:58 PIT >> INFO : SLAVE : used. Which one is undefined.

7:54:58 PIT >> INFO : Sending 2 test classes to slave
7:54:58 PIT >> INFO : Sent tests to slave
7:54:59 PIT >> INFO : SLAVE : 7:54:59 PIT >> INFO : Found  2 tests

7:54:59 PIT >> INFO : SLAVE : 7:54:59 PIT >> INFO : Dependency analysis reduced number of potential tests by 0

7:54:59 PIT >> INFO : SLAVE : 7:54:59 PIT >> INFO : 2 tests received

/-  7:55:00 PIT >> INFO : Calculated coverage in 1 seconds.
7:55:00 PIT >> INFO : Created  1 mutation test units
stderr  : objc[45722]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/bin/java and /Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be                                                                        stderr  : used. Which one is undefined.
/ 7:55:01 PIT >> INFO : Completed in 3 seconds
================================================================================
- Timings
================================================================================
> scan classpath : < 1 second
> coverage and dependency analysis : 1 seconds
> build mutation tests : < 1 second
> run mutation analysis : 1 seconds
--------------------------------------------------------------------------------
> Total  : 3 seconds
--------------------------------------------------------------------------------
================================================================================
- Statistics
================================================================================
>> Generated 14 mutations Killed 14 (100%)
>> Ran 14 tests (1 tests per mutation)
================================================================================
- Mutators
================================================================================
> org.pitest.mutationtest.engine.gregor.mutators.ConditionalsBoundaryMutator
>> Generated 2 Killed 2 (100%)
> KILLED 2 SURVIVED 0 TIMED_OUT 0 NON_VIABLE 0 
> MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0 
> NO_COVERAGE 0 
--------------------------------------------------------------------------------
> org.pitest.mutationtest.engine.gregor.mutators.ReturnValsMutator
>> Generated 4 Killed 4 (100%)
> KILLED 4 SURVIVED 0 TIMED_OUT 0 NON_VIABLE 0 
> MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0 
> NO_COVERAGE 0 
--------------------------------------------------------------------------------
> org.pitest.mutationtest.engine.gregor.mutators.MathMutator
>> Generated 3 Killed 3 (100%)
> KILLED 3 SURVIVED 0 TIMED_OUT 0 NON_VIABLE 0 
> MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0 
> NO_COVERAGE 0 
--------------------------------------------------------------------------------
> org.pitest.mutationtest.engine.gregor.mutators.NegateConditionalsMutator
>> Generated 5 Killed 5 (100%)
> KILLED 5 SURVIVED 0 TIMED_OUT 0 NON_VIABLE 0 
> MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0 
> NO_COVERAGE 0 
--------------------------------------------------------------------------------

BUILD SUCCESSFUL

Total time: 8.111 secs

レポートのHTML

f:id:kyon_mm:20141216063133p:plain

f:id:kyon_mm:20141216063137p:plain

f:id:kyon_mm:20141216063141p:plain

例えば、1<=number<=100の境界値がテストされていない場合。つまり、例外が投げられることを確認するテストが-10と200だけを使っているし、他のテストにおいても2から98くらいの間でしかテストを書いていない場合はこんなレポートになります。カバレッジが減っていますね。

f:id:kyon_mm:20141216064311p:plain

f:id:kyon_mm:20141216064315p:plain

f:id:kyon_mm:20141216064318p:plain

使いどころ

もちろん、これで完璧に境界値をテストしているとは言えないわけですが、単純にコードカバレッジでC1とかよりは信頼のおける数字で、僕が思う使いどころは基本的に2つあります。

  • 現在のテストがプロダクトコードの仕様化にどの程度貢献しているかを知る
  • テストコードを変更するときの確認

C1カバレッジがどうこうって言ったときに言えるのは「それっぽいテストを書いた」というくらいなので、少なくとももう少し機械的に「境界値分析したのかなぁ?」ということを全体をレビューしながらやるときの当たりをつけるのにも便利です。

テストコードを変更するときに、まずはミューテーションカバレッジを100%にしてから、それが保たれている事を確認しながら行うとテストコードへのバグ混入が少なくなります。

まとめ

いろんな言語でのミューテーションテストツールがもっと増えたら(僕が)うれしいです。各言語のASTいじるのが好きな人はこういったツールを実装するのも楽しいと思いますよ!