JUnit中級編:テストコードをもっと賢く書こう!

docs

JUnitでのテスト、もう慣れてきたかな? 前回の記事(JUnit実践:テストコードを書いてみよう! | ToolDocs)ではJUnitの基本的な使い方を解説したけど、今回は一歩踏み込んで、もっと効率的でパワフルなテストを書くためのテクニックを紹介していくよ。これで君のテストコードはもっと賢くなるはず!


テストの準備と後片付け:@BeforeEachと@AfterEach

テストメソッドごとに同じ初期設定が必要だったり、テスト後にリソースを解放したりしたいことってよくあるよね。そんなときに便利なのが@BeforeEach@AfterEachアノテーション。

@BeforeEachを付けたメソッドは、各テストメソッドが実行されるに毎回実行されるんだ。データベース接続の初期化とか、テストデータの準備なんかに使えるね。

一方、@AfterEachを付けたメソッドは、各テストメソッドが実行されたに毎回実行される。テスト中に作成した一時ファイルの削除とか、データベース接続のクローズなんかに役立つよ。

例を見てみよう

簡単な計算機クラスCalculatorをテストする例で考えてみよう。

Java

// Calculator.java
class Calculator {
    private int result;

    public Calculator() {
        this.result = 0;
    }

    public void add(int number) {
        this.result += number;
    }

    public int getResult() {
        return this.result;
    }
}

このCalculatorをテストするクラスCalculatorTestで、@BeforeEach@AfterEachを使ってみよう。

Java

// CalculatorTest.java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {
    Calculator calculator;

    // 各テストメソッドの前に実行される
    @BeforeEach
    void setUp() {
        System.out.println("--- setUp()が実行されました ---");
        calculator = new Calculator(); // 各テストで新しいCalculatorインスタンスを作成
    }

    // 各テストメソッドの後に実行される
    @AfterEach
    void tearDown() {
        System.out.println("--- tearDown()が実行されました ---");
        calculator = null; // オブジェクトをnullにする(リソース解放のイメージ)
    }

    @Test
    void testAdd() {
        System.out.println("testAdd()が実行されました");
        calculator.add(5);
        assertEquals(5, calculator.getResult());
    }

    @Test
    void testAddMultipleTimes() {
        System.out.println("testAddMultipleTimes()が実行されました");
        calculator.add(10);
        calculator.add(20);
        assertEquals(30, calculator.getResult());
    }
}

実行結果

--- setUp()が実行されました ---
testAdd()が実行されました
--- tearDown()が実行されました ---
--- setUp()が実行されました ---
testAddMultipleTimes()が実行されました
--- tearDown()が実行されました ---

実行結果を見ると、setUp()tearDown()がそれぞれのテストメソッドの前後でちゃんと実行されているのがわかるね。これでテスト間の独立性を保ちつつ、共通の準備・後処理を簡潔に書けるようになるよ。


特定の条件下でテストを実行:@Disabledと@EnabledIf

テストコードの中には、まだ開発中の機能のテストだったり、特定の環境でしか実行できないテストだったり、一時的に無効にしたいものが出てくることがあるよね。そんなときに使えるのが@Disabledアノテーションと、ちょっと応用的な@EnabledIfなどだ。

@Disabledで一時的に無効化

@Disabledをテストクラスやテストメソッドに付けると、そのテストはJUnitの実行対象から外れるよ。後で修正する予定のテストや、一時的に失敗するテストなんかをスキップしたいときに便利だ。

Java

// CalculatorTest.java (一部変更)
import org.junit.jupiter.api.Disabled;
// ... (他のimport文は省略)

class CalculatorTest {
    // ... (setUp, tearDownは省略)

    @Test
    void testAdd() {
        // ...
    }

    @Disabled("このテストは現在開発中です。後で有効にします。") // このテストは実行されない
    @Test
    void testSubtract() {
        System.out.println("testSubtract()が実行されました (スキップされるはず)");
        // まだ実装されていないsubtractメソッドのテスト(仮)
        // calculator.subtract(5);
        // assertEquals(-5, calculator.getResult());
    }
}

実行結果

testSubtract()は実行されず、JUnitの実行結果レポートには「スキップされたテスト」として表示されるよ。


例外のテスト:assertThrows

プログラムには、予期せぬ入力や状態に対して例外を投げるべきケースがあるよね。例えば、0で割ろうとしたらArithmeticExceptionを投げるとか。そんなとき、JUnitでその例外が正しく投げられているかをテストしたい。そこで使うのがassertThrowsだ。

assertThrowsは、指定した処理が特定の例外を発生させるかを検証するアサートメソッドだよ。

例を見てみよう

割り算を行うDividerクラスを例にしてみよう。0で割ろうとしたらIllegalArgumentExceptionを投げるようにするね。

Java

// Divider.java
class Divider {
    public double divide(double numerator, double denominator) {
        if (denominator == 0) {
            throw new IllegalArgumentException("0で割ることはできません!");
        }
        return numerator / denominator;
    }
}

このDividerクラスのテストで、0除算の例外を検証するよ。

Java

// DividerTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;

class DividerTest {

    @Test
    void testDivideByZeroThrowsException() {
        Divider divider = new Divider();

        // 0で割ったときにIllegalArgumentExceptionが投げられることを検証
        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
            divider.divide(10, 0);
        });

        // 例外メッセージが正しいかどうかも検証できる
        assertEquals("0で割ることはできません!", thrown.getMessage());
    }

    @Test
    void testDivideNormally() {
        Divider divider = new Divider();
        assertEquals(2.0, divider.divide(10, 5));
    }
}

実行結果

テスト実行中...
testDivideByZeroThrowsException(): PASSED
testDivideNormally(): PASSED

testDivideByZeroThrowsExceptionは、IllegalArgumentExceptionが期待通りに投げられたのでパスするよ。もし例外が投げられなかったり、別の種類の例外が投げられたりした場合は、このテストは失敗するんだ。


テストのグループ化と入れ子:@Nested

テストクラスの中に、関連するテストをさらにグループ化して整理したいことってあるよね? 例えば、ある機能のさまざまな状態に対するテストを、その状態ごとにまとめておきたいとか。そんなときに便利なのが@Nestedアノテーションだよ。

@Nestedを付けた内部クラスは、その外部クラスのテストのコンテキスト内で実行されるようになるんだ。これにより、より階層的で読みやすいテスト構造を作ることができるよ。

例を見てみよう

ユーザーの認証状態に応じた処理をテストする例を考えてみよう。

Java

// UserAuthenticator.java
class UserAuthenticator {
    public boolean isAuthenticated(String username, String password) {
        // 簡単な認証ロジック(実際はデータベースなどを使う)
        return "user".equals(username) && "pass".equals(password);
    }

    public String getUserRole(String username) {
        if ("admin".equals(username)) {
            return "Administrator";
        } else if ("user".equals(username)) {
            return "General User";
        }
        return "Guest";
    }
}

このUserAuthenticatorのテストで@Nestedを使ってみよう。

Java

// UserAuthenticatorTest.java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;

class UserAuthenticatorTest {

    UserAuthenticator authenticator;

    @BeforeEach
    void setUp() {
        authenticator = new UserAuthenticator();
    }

    @Nested
    class AuthenticationTests { // 認証に関するテストをグループ化

        @Test
        void testValidCredentials() {
            assertTrue(authenticator.isAuthenticated("user", "pass"));
        }

        @Test
        void testInvalidUsername() {
            assertFalse(authenticator.isAuthenticated("wronguser", "pass"));
        }

        @Test
        void testInvalidPassword() {
            assertFalse(authenticator.isAuthenticated("user", "wrongpass"));
        }
    }

    @Nested
    class UserRoleTests { // ユーザーロールに関するテストをグループ化

        @Test
        void testAdminRole() {
            assertEquals("Administrator", authenticator.getUserRole("admin"));
        }

        @Test
        void testGeneralUserRole() {
            assertEquals("General User", authenticator.getUserRole("user"));
        }

        @Test
        void testGuestRole() {
            assertEquals("Guest", authenticator.getUserRole("unknown"));
        }
    }
}

実行結果

JUnitの実行結果では、以下のように階層的に表示されることが多いよ(IDEによって表示は異なる)。

UserAuthenticatorTest
└── AuthenticationTests
    ├── testValidCredentials()
    ├── testInvalidUsername()
    └── testInvalidPassword()
└── UserRoleTests
    ├── testAdminRole()
    ├── testGeneralUserRole()
    └── testGuestRole()

このように@Nestedを使うことで、テストコードの構造がより明確になり、どのテストがどの機能に関連しているのかが一目でわかるようになるんだ。


まとめ

今回はJUnit中級編として、より実用的なテストコードを書くためのテクニックをいくつか紹介したよ。

  • @BeforeEach@AfterEach: 各テストメソッドの前後で共通の処理を実行するのに便利。テストの独立性を高められる。
  • @Disabled: 一時的にテストをスキップしたいときに使う。
  • assertThrows: 例外が正しく投げられることを検証するのに使う。エラーハンドリングのテストに必須。
  • @Nested: 関連するテストをグループ化して、テストコードの構造を読みやすくする。

これらのテクニックを使いこなせば、君のテストコードはもっと整理されて、より堅牢なものになるはずだ。テストは開発の質を上げるためにすごく重要だから、ぜひ今回学んだことを実践してみてね。

もっと深く学びたい人は、ぜひJUnitの公式ドキュメント(英語だけど、内容は充実してるよ!)を覗いてみるのもいいかもしれないね。そして、引き続き(ToolDocs | 様々なツールや技術情報を紹介します)でJUnitの旅を続けよう!

何か質問はあるかな? それとも、他に知りたいJUnitの機能があったりする?

コメント

タイトルとURLをコピーしました