okinawa

IT勉強メモ

Junit5を使ってみた感想

仕事で実際に使ってみて、かなり良かった!

なので良かった点を忘れないうちに書いておこう。

良かった点

バグ・考慮漏れに気づく。

主にNullチェック忘れ、if文の条件ミスの発見に役立った。

カバレッジ100%目指すだけで効果大

カバレッジ100%目指すのは賛否両論ありそうだけど、個人的には良かった。

とにかくカバレッジ100を目指せばいいので簡単。

通常のテストでは、テスト仕様書を書くのだけど、それだけだとどうしても考慮漏れが出てくる。

しかし、カバレッジ100目指すという明確な目標があるので、その点では考慮漏れが出ない。何より楽w

余談

ただし、カバレッジ100%を目指しつつ、真面目にメソッドの戻り値も検証するのは大変そう。

目的が異なるテストとして割り切って別々に作るのが良いかも?

  • カバレッジ100%:エラー出さずに最後まで走りきれるかという観点
  • 戻り値検証:境界値チェック・同地クラス分割・例外テスト

カバレッジ100は全体をざっと見るために作る(戻り値の検証は雑でOK)

戻り値検証でいわゆる通常のテストっぽいケースを書くイメージ。

意外だった点

検証部分は雑でも意外と効果を感じた。

assertEqualsの部分が肝だろうと思っていたけど、大きなメソッドになると戻り値だけに注目せず、途中の処理でエラー出ないか、条件分岐が正しいかをテストできるのも便利だった。

検証結果のエラーで気づくことより、テスト実行でぬるぽエラー出たり、カバレッジ100にならないのおかしいなあで色々気づくことが多かった。

コード例

・テスト対象クラス

package junit;

public class SampleBug {
    private static final String FLAG_ON = "1";
    private static final String FLAG_OFF = "0";
    private String flag;
    private String str;

    //戻り値はあえて意味不明にしています
    int sampleExecute() {

        if (str == null && flag.equals(FLAG_ON)) {
            System.out.println("FLAG_ON");

            //コピペの弊害。FLAG_OFFのつもりがFLAG_ONで判定している
        } else if (str == null && flag.equals(FLAG_ON)) {
            System.out.println("FLAG_OFF");
        }

        //substring前のnullチェック忘れ
        str.substring(0, 3);
        

        return 1;
    }

}

・テストクラス

package junit;

import static org.junit.jupiter.api.Assertions.*;

import java.lang.reflect.Field;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class SampleBugTest {
    SampleBug sampleBug;
    Field fieldStr;
    Field fieldFlag;
    
    @BeforeEach
    void setup() throws NoSuchFieldException, SecurityException {
        sampleBug = new SampleBug();
        
        //リフレクションを使ってテスト対象クラスの変数にアクセス
        //sampleBugクラスのstr変数を取得する
        fieldStr = sampleBug.getClass().getDeclaredField("str");
        //privateの場合でもアクセス可能にする
        fieldStr.setAccessible(true);
        
        fieldFlag = sampleBug.getClass().getDeclaredField("flag");
        fieldFlag.setAccessible(true);
        
    }
    
    @Test
    void test1() throws IllegalArgumentException, IllegalAccessException {
        //準備
        //テスト対象クラスのflag変数に1をセット
        fieldFlag.set(sampleBug, "1");
        
        //実行
        int actual = sampleBug.sampleExecute();
        
        //検証
        assertEquals(1, actual);
    }
    
    @Test
    void test2() throws IllegalArgumentException, IllegalAccessException {
        //準備
        //テスト対象クラスのflag変数に0をセット
        fieldFlag.set(sampleBug, "0");
        
        //実行
        int actual = sampleBug.sampleExecute();
        
        //検証
        assertEquals(1, actual);
    }

}

実際どんな感じでバグに気づけたのか

Nullチェック忘れ

test1を実行したら途中でコケた。

substring前のnullチェックが抜けてたわあ、と気付ける。

ということで直します。(本当は文字列の長さチェックも必要だけど省略)

if(str != null) {
    str.substring(0, 3);
}
if条件間違い

test1とtest2を実行してカバレッジを見てみたらelse ifの方が通っていなかった。

定数FLAG_OFFのつもりが、FLAG_ONになっていると気づけた。

修正後のコード

・テスト対象クラス

package junit;

public class SampleBug {
    private static final String FLAG_ON = "1";
    private static final String FLAG_OFF = "0";
    private String flag;
    private String str;

    //戻り値はあえて意味不明にしています
    int sampleExecute() {

        if (str == null && flag.equals(FLAG_ON)) {
            System.out.println("FLAG_ON");

            //FLAG_OFFに修正
        } else if (str == null && flag.equals(FLAG_OFF)) {
            System.out.println("FLAG_OFF");
        }

        //nullチェック追加
        if (str != null) {
            str.substring(0, 3);
        }

        return 1;
    }
}

・テストクラス

package junit;

import static org.junit.jupiter.api.Assertions.*;

import java.lang.reflect.Field;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class SampleBugTest {
    SampleBug sampleBug;
    Field fieldStr;
    Field fieldFlag;
    
    @BeforeEach
    void setup() throws NoSuchFieldException, SecurityException {
        sampleBug = new SampleBug();
        
        //リフレクションを使ってテスト対象クラスの変数にアクセス
        //sampleBugクラスのstr変数を取得する
        fieldStr = sampleBug.getClass().getDeclaredField("str");
        //privateの場合でもアクセス可能にする
        fieldStr.setAccessible(true);
        
        fieldFlag = sampleBug.getClass().getDeclaredField("flag");
        fieldFlag.setAccessible(true);
        
    }
    
    @Test
    void test1() throws IllegalArgumentException, IllegalAccessException {
        //準備
        //テスト対象クラスのflag変数に1をセット
        fieldFlag.set(sampleBug, "1");
        
        //実行
        int actual = sampleBug.sampleExecute();
        
        //検証
        assertEquals(1, actual);
    }
    
    @Test
    void test2() throws IllegalArgumentException, IllegalAccessException {
        //準備
        //テスト対象クラスのflag変数に0をセット
        fieldFlag.set(sampleBug, "0");
        
        //実行
        int actual = sampleBug.sampleExecute();
        
        //検証
        assertEquals(1, actual);
    }
    
    @Test
    void test3()  throws IllegalArgumentException, IllegalAccessException {
        //準備
        //テスト対象クラスのflag変数にあいうえおをセット
        fieldStr.set(sampleBug, "あいうえお");
        
        //実行
        int actual = sampleBug.sampleExecute();
        
        //検証
        assertEquals(1, actual);
    }

}

上記コードのカバレッジ結果

カバレッジの見方&操作

実行方法

テストクラスを右クリック→カバレッジJunit

カバレッジの見方

右サイドバーに注目。

SapleBug.javaカバレッジが100%になっている。

■コードの色の意味

  • 緑:OK
  • 黄色:通ったが全条件を網羅していない
  • 赤:通っていない

→詳しくは「Missed Instructions(分岐網羅)とMissed Branches(条件網羅)」のところに書いた。

エビデンスの取得方法

右サイドバーで右クリック→セッションのエクスポート

完了

下記のファイルが作られる。

中身

Missed Instructions(分岐網羅)は100%。

Missed Branches(条件網羅)は90%になっている。

Missed Instructions(分岐網羅)とMissed Branches(条件網羅)

分岐網羅

分岐網羅は文字通り全ての分岐を網羅したら100%になる。C1と表されることもある。

例えば下記のif文がtrueになるケースとfalseになるケースだけ作れば100%。

       if (str == null && flag.equals(FLAG_ON)) {
            System.out.println("FLAG_ON");

            //コピペの弊害。FLAG_OFFのつもりがFLAG_ONで判定している
        }
条件網羅

条件網羅は、個々の条件式の真偽をそれぞれ1回以上含むようにテストすることを条件網羅とをいいます。C2と表されることもあります。

個々の条件式とは、if文を1個と数えるのではなく、if文の中の条件式を1個と数える。

if(条件式1 && 条件式2 && 条件式3)

例えば下記のif文ではこの2ケースでは条件網羅できていない

  • (ケース1)条件式1=true && 条件式2=true
  • (ケース2)条件式1=false && 条件式2=true

条件式1はtrue/falseの条件が網羅されているが、条件式2はtrueしか条件が無いから。

       //条件式1 && 条件式2
        if (str == null && flag.equals(FLAG_ON)) {
            System.out.println("FLAG_ON");
        }

なので、もう一つ下記のケースを足す必要がある

  • (ケース3)条件式1=true && 条件式2=false
条件網羅するテストコードを書いてみる

・テスト対象

       //条件式1 && 条件式2
        if (str == null && flag.equals(FLAG_ON)) {
            System.out.println("FLAG_ON");
        }

・テストクラス(分岐網羅はしているが条件網羅はしていない例)

//importや@BeforeEachは省略
    
    @Test
    void test1() throws IllegalArgumentException, IllegalAccessException {
        //準備
        //テスト対象クラスのflag変数に1をセット
        fieldStr.set(sampleBug, null);//条件式1=true
        fieldFlag.set(sampleBug, "1");//条件式2=true
        
        //実行
        int actual = sampleBug.sampleExecute();
        
        //検証
        assertEquals(1, actual);
    }
    
    @Test
    void test2() throws IllegalArgumentException, IllegalAccessException {
        //準備
        //テスト対象クラスのflag変数に1をセット
        fieldStr.set(sampleBug, "あいうえお");//条件式1=flse
        fieldFlag.set(sampleBug, "1");//条件式2=true
        
        //実行
        int actual = sampleBug.sampleExecute();
        
        //検証
        assertEquals(1, actual);
    }

カバレッジ実行

■コードの色の意味

  • 緑:OK
  • 黄色:通ったが全条件を網羅していない
  • 赤:通っていない

↓ テストコードに下記を追加。

  • 条件式1=true && 条件式2=false
   @Test
    void test3() throws IllegalArgumentException, IllegalAccessException {
        //準備
        //準備
        fieldStr.set(sampleBug, null);//条件式1=true
        fieldFlag.set(sampleBug, "0");//条件式2=false
        
        //実行
        int actual = sampleBug.sampleExecute();
        
        //検証
        assertEquals(1, actual);
    }

カバレッジ実行

全て緑になった!

MissedBranches(条件網羅)もちゃんと100%になってる!

まとめ

カバレッジ100%を目指すと、Nullチェック漏れやif文の条件間違いに気づける。(分岐網羅100%で良いと思う。)

エクセルでテスト仕様書を書くより楽しい。

カバレッジ100%&戻り値の検証を同時にやろうとすると大変そう。目的は別にあると割り切ったほうが良さそう。

  • カバレッジ100%:エラー出さずに最後まで走りきれるかという観点
  • 戻り値検証:境界値チェック・同地クラス分割・例外テスト

分岐網羅と条件網羅の参考

カバレッジの種類~C0・C1・C2・MCC~ - NRIネットコムBlog