自動テストのしんどい所
自動テストってありがたいですよね。コードが壊れてもすぐ気付ける、というのはコードに対する不安感を払拭してくれます。
ですが、これをプロジェクトに導入しようとするとあら大変。「カバレッジ100%しか認めん!!」なんて言われて、実装には直接関係ないテストコードを量産しなければなりません。
そうなると自動化をしたくなるのが人の性ですが、無償ツールだとテンプレートベースなので結局テンプレートは作る必要があったり、可読性に難がありメンテができない、ということで、これはこれでしんどいです。1
そんなめんどくささに対応するべく生み出されたのがDiffblueです。
Diffblueとは?
DiffBlueは機械学習を使用して、Java用のユニットテストを自動生成するツールです。ゴールドマン・サックスやAWSでも利用されているようです。今までは有償ツールだったのですが、9/9についにCommunity Editionが公開されました。2
読みやすいテストが特徴だそうです。
#環境とか
用意するツールとバージョン
Community EditionだとIntelliJ IDEAのプラグインのみなので、IntelliJ IDEAを導入します。
IntelliJ IDEA:2019.33
Diffblue:diffblue-cover-ij-2020.11.04-eval-jdk8-2020.2
導入方法
- DiffBlueコミュニティ版をダウンロード
- IntelliJ IDEAを起動し、ファイル>設定>プラグインを開く
- ⚙マークから「ディスクからプラグインをインストール」を選択
- 1.でダウンロードしたzipファイルを選択
- ライブラリが読み込まれたら再起動
実行方法
特性
分岐は必要最小限のパターンで網羅する
DiffBlueは最小限のパターン数で分岐を網羅しようとします。意図通りの挙動を試験するとは限りません。
例えば、数字をFizzBuzzに変換するコードであれば、最低限1,3,5,15を試験してほしい所ですが、以下のようなコードであれば、0,1しか試験しません。
public static String fizzBuzz(Integer i) {
String result = "";
if(i % 3 == 0){
result += "Fizz";
}
if(i % 5 == 0){
result += "Buzz";
}
if(result.equals("")){
result = i.toString();
}
return result;
}
@Test
public void testFizzBuzz() {
// Arrange
int i = 1;
// Act
String actualFizzBuzzResult = FizzBuzz.fizzBuzz(i);
// Assert
assertEquals("1", actualFizzBuzzResult);
}
@Test
public void testFizzBuzz2() {
// Arrange
int i = 0;
// Act
String actualFizzBuzzResult = FizzBuzz.fizzBuzz(i);
// Assert
assertEquals("FizzBuzz", actualFizzBuzzResult);
}
ループのテストケースが不十分
ループ処理のテストケースは、「分岐を全て網羅できるように」しか生成されません。
例えば以下の「nからmまでの数字を足し合わせる」プログラムでは、テストケースは1ケースしか生成されませんでした。1回ループすれば全て網羅できるからです。
public static Integer sumFor(Integer n,Integer m){
Integer sum = 0;
for (Integer i = n; i <= m; i++){
sum += i;
}
return sum;
}
@Test
public void testSumFor() {
assertEquals(1, SumFor.sumFor(1, 1).intValue());
}
ジェネリクスを使用した変数が引数に取られていると正常に動作しない
個人的に一番驚いたのがこれです。以下のコードのテストが生成できません。
import java.util.List;
public class SumForList {
public static Integer sumForList(List<Integer> list){
Integer sum = 0;
for(Integer num : list){
sum += num;
}
return sum;
}
}
エラーログは以下の通り。
14:56 Creating tests for method: SumForList.sumForList
14:56 Unable to generate test inputs not throwing a trivial exception.: 例外: java.lang.NullPointerException. We failed to find inputs to the method under test which would NOT lead to throwing of a trivial exception, like NullPointerException or LinkageError.
14:56 Test creation complete: Created 0 tests in total for 0 methods from 0 source classes
型変数に入れたクラスのインスタンスをどう生成すればいいかわからないようです。
引数をArrayにする、ラッパークラスを作りいい感じにイテレーションする、等すればテストを生成できます。
public static Integer sumForArray(Integer[] array){ //引数を配列に変更
List<Integer> list = Arrays.asList(array); //内部でListへ格納
Integer sum = 0;
for(Integer num : list){
sum += num;
}
return sum;
}
状態を持つクラスのテストにはgetter/equals/hashcodeの実装が必須
List等のラッパークラスを作ろう、と思っても落とし穴があります。
以下のように、パラメータのgetter/equals/hashcodeがないクラスの場合、テストケースが生成できません。
IDEの機能でさくっと生成してあげましょう。
public class ListWrap {
private List<Integer> numList;
public ListWrap(){
numList = new ArrayList<Integer>();
}
public void addNum(Integer num){
numList.add(num);
}
public Integer max(){
Integer max = null;
for(Integer num : numList){
if(max == null || max < num){
max = num;
}
}
return max;
}
エラーの内容は以下の通り。sizeやtoStringも実装するよう書かれていますが、試した限りでは不要でした。
16:04 Creating tests for method: NumStat.
16:04 Nothing to assert: the constructed class does not have observers (e.g. getters or public fields).: Class class com.company.NumStat does not have observers. Observer methods are parameterless, read-only methods that return the state of the class, for instance, getters, size(), toString().
なお、何らかの値を設定する方法があれば、setterは不要です。(今回でいうとaddNum)
privateメソッドのテストコードは生成されないが、呼び出し元メソッドの試験で検証される
DiffBlueはprivateメソッドのテストコードは生成しません。4その代わり、試験生成対象メソッドで呼び出し先のprivateメソッドまで考慮し、テストパターンを作るようです。
以下のコードでは、三角形の条件を「0以下の辺がないこと」「ある辺が他の辺の合計より長くないこと」と分け、それぞれ別メソッドで検証しています。
「0以下の辺がないこと」:テストケース2,4,6
「ある辺が他の辺の合計より長くないこと」:テストケース3,5,7
でそれぞれ検証していますね。
public class AssertTriangle {
/**
* 三角形の辺としての整合性チェック
* @param a
* @param b
* @param c
* @return
*/
public static boolean assertTriangle(int a,int b,int c){
if(hasNegativeSide(a,b,c)) return false;
return sideIsValid(a,b,c);
}
/**
* 三角形として成立しない辺がないかチェック
* @param a
* @param b
* @param c
* @return
*/
private static boolean sideIsValid(int a, int b, int c) {
return firstSideIsShorterThenOthersSum(a,b,c)
&& firstSideIsShorterThenOthersSum(b,c,a)
&& firstSideIsShorterThenOthersSum(c,a,b);
}
/**
* 辺の長さが0以下の値がないかチェック
* @param a
* @param b
* @param c
* @return
*/
private static boolean hasNegativeSide(int a, int b, int c){
return a <= 0 || b <= 0 || c <= 0;
}
/**
* 辺firstが辺second+辺thirdより短いかチェック
* @param
* @return
*/
private static boolean firstSideIsShorterThenOthersSum(int first,int second,int third){
return first < (second + third);
}
}
public class AssertTriangleTest {
@Test
public void testAssertTriangle() {
// Arrange
int a = 1;
int b = 1;
int c = 1;
// Act
boolean actualAssertTriangleResult = AssertTriangle.assertTriangle(a, b, c);
// Assert
assertTrue(actualAssertTriangleResult);
}
@Test
public void testAssertTriangle2() {
// Arrange
int a = 0;
int b = 1;
int c = 1;
// Act
boolean actualAssertTriangleResult = AssertTriangle.assertTriangle(a, b, c);
// Assert
assertFalse(actualAssertTriangleResult);
}
@Test
public void testAssertTriangle3() {
// Arrange
int a = 2;
int b = 1;
int c = 1;
// Act
boolean actualAssertTriangleResult = AssertTriangle.assertTriangle(a, b, c);
// Assert
assertFalse(actualAssertTriangleResult);
}
@Test
public void testAssertTriangle4() {
// Arrange
int a = 1;
int b = 0;
int c = 1;
// Act
boolean actualAssertTriangleResult = AssertTriangle.assertTriangle(a, b, c);
// Assert
assertFalse(actualAssertTriangleResult);
}
@Test
public void testAssertTriangle5() {
// Arrange
int a = 1;
int b = 2;
int c = 1;
// Act
boolean actualAssertTriangleResult = AssertTriangle.assertTriangle(a, b, c);
// Assert
assertFalse(actualAssertTriangleResult);
}
@Test
public void testAssertTriangle6() {
// Arrange
int a = 1;
int b = 1;
int c = 0;
// Act
boolean actualAssertTriangleResult = AssertTriangle.assertTriangle(a, b, c);
// Assert
assertFalse(actualAssertTriangleResult);
}
@Test
public void testAssertTriangle7() {
// Arrange
int a = 1;
int b = 1;
int c = 2;
// Act
boolean actualAssertTriangleResult = AssertTriangle.assertTriangle(a, b, c);
// Assert
assertFalse(actualAssertTriangleResult);
}
}
所感
正常系を拾うのは苦手そうだと感じました。FizzBuzzで3とか5とかチェックしないのは正直びっくりです。
ですが、逆に0等の予期しない値を突っ込んでくれるので、TDD等で正常系のテストコードが出来ている状態で実行するとお手軽にカバレッジが上がっていいかなと思いました。
#参考サイト
DiffBlue公式
Diffblueを使ってユニットテストを自動生成する | Developers.IO
-
2020.3verが最新ですが、日本語化がうまくできないため2019.3verを使っています ↩
-
もともとはprivateメソッドも、リフレクション等を駆使して生成していたようですが、やめたようです。Why does Diffblue not use reflection? ↩