Thoughtworks社が半年ごとに公開しているTechnology RadarのVol.23が最近公開されていたので一通り読んでみました。 その中でMutation testingという技術が気になったので、今回はこれについて書いていきます。
Mutation testingとは
Mutation testingとは、プロダクションコードに対するテストコードがどれだけ十分なものか、というテストの品質自体を評価するテスト手法です。
Mutation testingは、プロダクションコードに対してmutantと呼ばれる小さい変更を加えた後に既存のテストを実行し、テストが失敗するかどうかを検証します。検証した結果テストが失敗した場合は、実行したテストにはプロダクションコードの不備を検知できる能力があるとみなします。mutantによってテストが失敗した場合、mutantがkillされる、と表現されます。 プロダクションコードに対して大量のmutantを加えたときに、テストがどれだけmutantをkillできるかという割合を可視化することで、テストコードの品質を評価します。
Googleが2018年に公開した論文State of Mutation Testing at Googleでは、mutantのパターンとして以下が述べられています。
- AOR Arithmetic operator replacement
- a + b を a, b, a-b, a*b, a/b, a%b のどれかにランダムで書き換える
- LCR Logical connector replacement
- a && b を a, b, a||b, true, false のどれかにランダムで書き換える
- ROR Relational operator replacement
- a > b を a<b, a<=b, a>=b, true, false のどれかにランダムで書き換える
- UOI Unary operator insertion
- a を a++, a--, !a のどれかにランダムで書き換える
- SBR Statement block removal
- statement blockを空に書き換える
例えば、以下のようなコードがあった場合には、
if (a > b) { // do something }
Mutation testingによって、以下のようなmutantが生成されます。
if (a < b) { // do something }
こうした変更がされたプロダクションコードに対してテストを実行したときに、テストが失敗しない場合、 意味のないテストコードが書かれているということを開発者は知ることができます。
Pitestとは
PitestはJavaやKotlinなどのJVM言語でMutation testingを行うことができるライブラリです。 ソースコードはGitHub上にApache License 2.0で公開されています。
Pitestは、バイトコードに対してmutantを自動で生成した後、テストを自動で実行することでmutantをどれだけの割合killできたかを計測します。 計測した結果はhtmlファイルで生成されるので、プロダクションコードに対してどんなmutantが生成されたか、生成されたmutantがkillされたか、などをブラウザ上で確認することができます。
PitestはJVM言語を対象にしていますが、他に似たようなライブラリとしてStrykerというライブラリがTechnology Radar Vol.23で紹介されていました。 こちらは、JS, C#, Scalaをサポートしており、ソースコードはPitest同様GitHub上にApache License 2.0で公開されています。
Pitestを使ってMutation testingを試してみる
ここからは実際にPitestを使ってMutation testingを試してみたいと思います。
Pitestは、MavenやGradleやCLIなどのチュートリアルを用意しているので、今回はMavenを使って実装していきます。 サンプルコードはGitHubに公開しているので、気になる方はこちらを見てください。
サンプルコードの説明
今回はFizzBuzzを題材とした、以下のクラスをテストします。 引数で渡された整数に対してFizzBuzzの規則に沿った文字列を返すクラスです。
public class FizzBuzzGenerator { public String generate(int number) { if (number % 3 == 0 && number % 5 == 0) { return "FizzBuzz"; } if (number % 3 == 0) { return "Fizz"; } if (number % 5 == 0) { return "Buzz"; } return String.valueOf(number); } }
テストコードは以下です。
PitestはデフォルトではJUnit5をサポートしていないため今回はJUnit4を利用しています。 JUnit5はこちらを追加すれば動作するようですが、マイナーバージョンで開発されているため、試していませんがバージョンによっては不安定な動作をするかもしれません。
import org.junit.Test; import static org.junit.Assert.assertEquals; public class TestFizzBuzzGenerator { FizzBuzzGenerator fizzBuzzGenerator = new FizzBuzzGenerator(); @Test public void returnFizzBuzzIfInputIsDivisibleByThreeAndFive() { int input = 30; String actual = fizzBuzzGenerator.generate(input); // mutantをkillできないテストを再現するためassertionをコメントアウトしている // assertEquals("FizzBuzz", actual); } @Test public void returnFizzIfInputIsDivisibleByThree() { int input = 6; String actual = fizzBuzzGenerator.generate(input); assertEquals("Fizz", actual); } @Test public void returnBuzzIfInputIsDivisibleByFive() { int input = 10; String actual = fizzBuzzGenerator.generate(input); assertEquals("Buzz", actual); } @Test public void returnNumberIfInputIsIndivisibleByThreeOrFive() { int input = 13; String actual = fizzBuzzGenerator.generate(input); assertEquals(String.valueOf(input), actual); } }
pom.xmlの修正
pom.xmlにpitest-mavenをpluginとして追加します。
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> </plugin> <plugin> <groupId>org.pitest</groupId> <artifactId>pitest-maven</artifactId> <version>1.5.2</version> <!-- if you want to specify target classes, add following code. <configuration> <targetClasses> <param>com.your.package.root.want.to.mutate*</param> </targetClasses> <targetTests> <param>com.your.package.root*</param> </targetTests> </configuration> --> </plugin> </plugins> </build>
デフォルトでは、プロジェクトのプロダクションコード全てに対してmutantを生成しますが、pluginのconfigurationで対象のプロダクションコードやテストコードを指定することができます。
pluginのgoalを実行
pom.xmlに追加したpluginに定義されたmutationCoverage goalを実行します。
Pitestはバイトコードに対してMutation testingを行うため、バイトコードがない場合は事前にmvn compile
などを実行してコンパイルをしておくようにしてください。
$ mvn org.pitest:pitest-maven:mutationCoverage // 省略 23:22:01 PIT >> INFO : Verbose logging is disabled. If you encounter a problem, please enable it before reporting an issue. 23:22:01 PIT >> INFO : Sending 3 test classes to minion 23:22:01 PIT >> INFO : Sent tests to minion 23:22:01 PIT >> INFO : MINION : 23:22:01 PIT >> INFO : Checking environment 23:22:01 PIT >> INFO : MINION : 23:22:01 PIT >> INFO : Found 4 tests 23:22:01 PIT >> INFO : MINION : 23:22:01 PIT >> INFO : Dependency analysis reduced number of potential tests by 0 23:22:01 PIT >> INFO : 4 tests received 23:22:01 PIT >> INFO : Calculated coverage in 0 seconds. 23:22:02 PIT >> INFO : Incremental analysis reduced number of mutations by 0 23:22:02 PIT >> INFO : Created 2 mutation test units 23:22:02 PIT >> INFO : Completed in 1 seconds ================================================================================ - Mutators ================================================================================ > org.pitest.mutationtest.engine.gregor.mutators.EmptyObjectReturnValsMutator >> Generated 4 Killed 3 (75%) > KILLED 3 SURVIVED 1 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.ConditionalsBoundaryMutator >> Generated 1 Killed 0 (0%) > KILLED 0 SURVIVED 0 TIMED_OUT 0 NON_VIABLE 0 > MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0 > NO_COVERAGE 1 -------------------------------------------------------------------------------- > org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator >> Generated 1 Killed 0 (0%) > KILLED 0 SURVIVED 0 TIMED_OUT 0 NON_VIABLE 0 > MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0 > NO_COVERAGE 1 -------------------------------------------------------------------------------- > org.pitest.mutationtest.engine.gregor.mutators.MathMutator >> Generated 4 Killed 2 (50%) > KILLED 2 SURVIVED 2 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 4 (80%) > KILLED 4 SURVIVED 0 TIMED_OUT 0 NON_VIABLE 0 > MEMORY_ERROR 0 NOT_STARTED 0 STARTED 0 RUN_ERROR 0 > NO_COVERAGE 1 -------------------------------------------------------------------------------- ================================================================================ - Timings ================================================================================ > scan classpath : < 1 second > coverage and dependency analysis : < 1 second > build mutation tests : < 1 second > run mutation analysis : < 1 second -------------------------------------------------------------------------------- > Total : 1 seconds -------------------------------------------------------------------------------- ================================================================================ - Statistics ================================================================================ >> Generated 15 mutations Killed 9 (60%) >> Ran 22 tests (1.47 tests per mutation) // 省略
実行すると、上記のようにプロダクションコードに対してどれだけのmutantが生成され、テストコードによってどれだけkillされたか、実行にどれぐらい時間がかかったか、実行したテストの数、などが表示されます。
また、mutationCoverage goalが実行されると、計測結果をレポートしたhtmlファイルがtarget/pit-reports/YYYYMMDDHHMI
という場所に生成されます。
確認
生成されたhtmlファイルを開くと、以下のような計測結果画面が表示されます。
各ファイルのカバレッジが表示されています。
また、各ファイルのリンクに飛ぶと、以下のようにどのプロダクションコードにどういったmutantを生成したか、生成したmutantがkillsされたか、などが表示されています。
まとめ
今回はTechnology Radarで興味を持ったMutation testingについて紹介し、JVM言語でMutation testingを行うPitestのサンプルコードを実装しました。
Technology RadarでもTrialとして紹介されている通り、Pitestの活動の雰囲気やエコシステムを見ると、まだまだ安定したライブラリというわけではないので、テストライブラリのサポートやJava以外のJVM言語のサポートなどで手が届かないところがあるのかなと感じました。
ただ、Mutation testingという考え方自体は、とても面白いアイデアだと思いますし、パイプラインに組み込んでコードの品質を可視化する情報としては有用だと思うので、今後の機能や開発を楽しみにしたいと思います。
参考文献
- Goran Petrovic Marko Ivankovic. Proceedings of the 40th International Conference on Software Engineering 2017 (SEIP) (2018) (to appear). https://research.google/pubs/pub46584/
- Mutation testing. Wikipedia. https://en.wikipedia.org/wiki/Mutation_testing
- 第86回 GoogleにおけるMutation Testの実践(パート1) (中井悦司) https://www.school.ctc-g.co.jp/columns/nakai2/nakai286.html