Developersをフォローする

JUnit5を使った自動テストを試してみた

バックエンド

実装したJavaのプログラムが正しく動くかどうか、みなさん確認しているかと思います。
複雑なプログラムを網羅的にテストするには、
全て手動ではテスト漏れが発生してしまいますよね。

テストを自動化できれば、デグレードを未然に防ぐことが出来るかもしれません。
今回は単体テストでよく使用されるJunitを試してみたいと思います。

環境

Junit:5.9.0
JDK:17
IDE:IntelliJ IDEA

テスト対象となるクラスの仕様

購入金額と購入回数に応じて会員ランクの値を返す。

購入金額購入回数付与ランク
10000未満または0回ランク無し
10000円以上かつ1回以上BEGINNER
 20000円以上2回以上REGULAR
30000円以上3回以上BRONZE
 40000円以上4回以上SILVER
50000円以上5回以上GOLD
60000円以上6回以上PLATINUM

各クラスのソース

購入金額と購入回数を保持する。

public class User {

    public User() {
    }

    public User( String name, long purchase, int numPurchases) {
        this.purchase = purchase;
        this.numPurchases = numPurchases;
        this.name = name;
    }

    // 会員ランク種別
    private RankType rankType;
    // 購入金額
    private long purchase;
    // 購入回数
    private int numPurchases;
    // 氏名
    private String name;

    // 以下、getter,setterを追記

Rankクラス

rankAllocationProcessメソッド
Userオブジェクトを仮引数で受取、
対象となるユーザの購入金額と購入回数によって会員ランクを返す。

platinumPointメソッド
PLATINUM会員ならTRUEを返す単純な処理。

public class Rank {

    public static long SUFFICIENT = 150000;

    public RankType rankAllocationProcess(User user) {

        // 購入金額
        long purchases = user.getPurchase();
        // 購入回数
        int n = user.getNumPurchases();

        if(n == 0 || purchases < 10000){
            return NON;
        } else if (n >= 1 && purchases < 20000 || n == 1) {
            return BEGINNER;
        } else if (n >= 2 && purchases < 30000 || n == 2) {
            return REGULAR;
        } else if (n >= 3 && purchases < 40000 || n == 3) {
            return BRONZE;
        } else if (n >= 4 && purchases < 50000 || n == 4) {
            return SILVER;
        } else if (n >= 5 && purchases < 60000 || n == 5) {
            return GOLD;
        } else {
            return PLATINUM;
        }
    }

    public boolean platinumPoint(RankType rank){
        return rank.equals(PLATINUM) ? true : false;
    }
}

ランクの値を持つ列挙型

package rank;

public enum RankType {
    NON,
    BEGINNER,
    REGULAR,
    BRONZE,
    SILVER,
    GOLD,
    PLATINUM
}

テストコード

ManualRankTestクラス
このクラスの各メソッドは、意図した購入金額と購入回数をUserオブジェクトに設定し、ranks.rankAllocationProcessメソッドに渡します。
戻り値が予想通りの値が返ってくるかをassertEqualsメソッドを使用して確認します。
※assertEquals(予想する値, 実績値)

CsvRankTestクラス
このクラスのメソッドは、CSVにあるデータを1行ずつ実行し、会員ランクがPLATINUMの場合はtureを、それ以外の場合はfalseが返ってきます。
戻り値に対しては、trueの場合とfalseの場合にそれぞれ違う処理が実行されるようにassumingThatメソッドを使用しています。

class RankTest {

    @Nested
    class ManualRankTest{
        User user = new User();
        private final Rank ranks = new Rank();

        @Test
        @DisplayName("購入金額:0円/購入回数:0")
        public void testRank0(){
            user.setName("クラウド スミス0");
            user.setPurchase(0);
            user.setNumPurchases(0);

            RankType rank = ranks.rankAllocationProcess(user);

            assertEquals(NON, rank);
        }

        @Test
        @DisplayName("購入金額:10000円/購入回数:1")
        public void testRank1(){
            user.setName("クラウド スミス1");
            user.setPurchase(10000);
            user.setNumPurchases(1);

            RankType rank = ranks.rankAllocationProcess(user);

            assertEquals(BEGINNER, rank);
        }

        // 以下、省略(testRank2~testRank6のテストケースを作成)

        @Test
        @DisplayName("購入金額:100000円/購入回数:1 BEGINNERとなるが予想をPLATINUMとしている場合")
        public void testRankErrer(){
            user.setName("クラウド スミス7");
            user.setPurchase(100000);
            user.setNumPurchases(1);

            RankType rank = ranks.rankAllocationProcess(user);

            assertEquals(PLATINUM, rank);
        }

    }

    @Nested
    class CsvRankTest{
        private final Rank ranks = new Rank();

        @ParameterizedTest
        @CsvFileSource(resources = {"/member_data.csv"})
        public void testRank(String name, long purchase, int n){
            User user = new User(name, purchase, n);

            RankType rank = ranks.rankAllocationProcess(user);

            assumingThat(
                    rank.equals(PLATINUM), () ->{
                        boolean platinumFlag = ranks.platinumPoint(rank);
                        assertTrue(platinumFlag);
                    }
            );

            assumingThat(
                    !rank.equals(PLATINUM), () ->{
                        boolean platinumFlag = ranks.platinumPoint(rank);
                        assertFalse(platinumFlag);
                    }
            );
        }
    }
}

CSVデータ(member_data.csv)

member_data.csv
クラウド スミス, 19999, 2
クラウド スミス, 25480, 2
クラウド スミス, 65250, 4
クラウド スミス, 29999, 6
クラウド スミス, 30000, 7
クラウド スミス, 30001, 20
クラウド スミス, 55000, 5
クラウド スミス, 55000, 10
クラウド スミス, 55000, 20
クラウド スミス, 12000000, 1

テストを実行

実行結果は下記のようになりました。

テストクラスのtestRankErrerメソッドにて、
assertEqualsに設定した予想値と実値が不一致のためエラーが発生しています。
この場合、実装したソースが間違っていないかやテストソースが間違っていないかを確認する必要があります。
今回はテストケースが間違っていたので、assertEqualsの予想値を正しく設定し再度実行することで、全て正常終了しました。

今回使用したアノテーションの説明

@Nested
 Nestedは入れ子という意味。
 @Nestedを宣言することで2つ以上のテストクラスを入れられるようになります。

@Test
 テストメソッドであることを宣言します。

@DisplayName
 テスト実行後にコンソールに出力される文字列です。
 IntelliJ IDEAでは、以下のように表示されます。

@ParameterizedTest
 @Testと同じくテストメソッドであることを宣言します。
 違いはパラメータを受取って、複数回実行することが出来ます。

@CsvFileSource
 引数にCSVファイルのパスを設定する事で、CSVの格納データをカンマ区切りでテストメソッドの各引数に設定します。
 @ParameterizedTestと併用し、1回実行されるたびにCSVデータを1行取得してくれます。

感想

一度作成してしまえば、あとは実行するだけで、
対象ソースの修正が発生しても、すぐにデグレチェックができます。

ただ、テストケースが膨大になると管理が大変になるので、テスト用のパラメータだけを持つテストクラスを作成するなどして、クラスを分けることも必要になりそうです。

@CsvFileSourceに関しては、実際のCSVデータを顧客からもらうことが多いと思いますので、大量データの確認には便利そうです。

まだまだ、便利なJUnitの機能があるので色々と試そうと思います。

自動テストの必要性

新規開発からテストソースを作成していれば、運用段階での修正や機能追加などでデグレチェックが可能なので、品質は高くなります。

ただ、テストソースの作成やメンテナンスによるコストを考慮する必要もあったり、納期などの兼ね合いで実施しなかったりもあると思いますが、その場合は、重要な機能のみや、複雑な条件分岐がある機能など、自動テストのメリットが活かせる箇所のみ実施することもできますので、検討の余地はありそうですね。