3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

AIによるテスト自動生成ツール「Diffblue」を使ってみた

Last updated at Posted at 2020-12-13

自動テストのしんどい所

自動テストってありがたいですよね。コードが壊れてもすぐ気付ける、というのはコードに対する不安感を払拭してくれます。
ですが、これをプロジェクトに導入しようとするとあら大変。「カバレッジ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

導入方法

  1. DiffBlueコミュニティ版をダウンロード
  2. IntelliJ IDEAを起動し、ファイル>設定>プラグインを開く
  3. ⚙マークから「ディスクからプラグインをインストール」を選択
    image.png
  4. 1.でダウンロードしたzipファイルを選択
  5. ライブラリが読み込まれたら再起動
    image.png

実行方法

  1. コードを書く
  2. プロジェクトエクスプローラーで右クリックし、Write Testsをクリック
    image.png

特性

分岐は必要最小限のパターンで網羅する

DiffBlueは最小限のパターン数で分岐を網羅しようとします。意図通りの挙動を試験するとは限りません。
例えば、数字をFizzBuzzに変換するコードであれば、最低限1,3,5,15を試験してほしい所ですが、以下のようなコードであれば、0,1しか試験しません。

FizzBuzz.java
    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;
    }

FizzBuzzTest.java
    @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回ループすれば全て網羅できるからです。

SumFor.java
    public static Integer sumFor(Integer n,Integer m){
        Integer sum = 0;
        for (Integer i = n; i <= m; i++){
            sum += i;
        }
        return sum;
    }

SumForTest.java
    @Test
    public void testSumFor() {
        assertEquals(1, SumFor.sumFor(1, 1).intValue());
    }

ジェネリクスを使用した変数が引数に取られていると正常に動作しない

個人的に一番驚いたのがこれです。以下のコードのテストが生成できません。

SumForList.java
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にする、ラッパークラスを作りいい感じにイテレーションする、等すればテストを生成できます。

sumForArray.java
    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の機能でさくっと生成してあげましょう。

ListWrap.java

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
でそれぞれ検証していますね。

AssertTriangle.java
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);
    }

}

TriangleServiceTest.java
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

  1. ユニットテストの自動作成ツールを調べてみた(2019年末版)

  2. 自動JavaユニットテストツールDiffblueの無料コミュニティエディションが登場

  3. 2020.3verが最新ですが、日本語化がうまくできないため2019.3verを使っています

  4. もともとはprivateメソッドも、リフレクション等を駆使して生成していたようですが、やめたようです。Why does Diffblue not use reflection?

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?