JUnit徹底活用!テストの質を高める上級テクニック

docs

前回(■ – Hello Java World)はJUnitの基本的な使い方を紹介したけど、今回はさらに踏み込んだ内容を解説していくよ。「JUnitなんて@Testつけてメソッド書くだけでしょ?」って思ってるそこのアナタ、それはもったいない!JUnitにはテストコードの質を上げて、開発を効率化するための便利な機能がたくさんあるんだ。

パラメータ化テストでテストパターンを網羅!

同じようなテストを何度も書くのって面倒だよね?例えば、メソッドの引数によって戻り値が変わるような場合、引数の組み合わせごとに@Testメソッドを書いてたらキリがない。そんな時に役立つのがパラメータ化テストだよ。これを使えば、複数のテストデータを1つのテストメソッドでまとめて実行できるんだ。

JUnit 5では、@ParameterizedTestアノテーションと、テストデータを供給するためのいくつかのアノテーションを組み合わせて使うよ。

@ValueSourceでシンプルなデータを使う

文字列や数値といったシンプルなデータをテストに使いたいなら、@ValueSourceが便利だよ。

Java

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class StringUtilsTest {

    // 文字列が空でないことをテストするメソッド
    public boolean isNotEmpty(String str) {
        return str != null && !str.isEmpty();
    }

    @ParameterizedTest
    @ValueSource(strings = {"hello", "world", "JUnit"})
    void testIsNotEmpty(String word) {
        assertTrue(isNotEmpty(word));
    }
}

実行結果:

testIsNotEmpty(String) [1] hello -> PASS
testIsNotEmpty(String) [2] world -> PASS
testIsNotEmpty(String) [3] JUnit -> PASS

この例では、testIsNotEmptyメソッドが”hello”, “world”, “JUnit”という3つの文字列それぞれで実行されるんだ。

@CsvSourceで複数の引数を使う

複数の引数を組み合わせてテストしたい場合は、@CsvSourceを使ってみよう。カンマ区切りでデータを指定できるから、CSVみたいに使えるよ。

Java

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {

    // 足し算を行うメソッド
    public int add(int a, int b) {
        return a + b;
    }

    @ParameterizedTest
    @CsvSource({
        "1, 2, 3",
        "5, 5, 10",
        "10, -3, 7"
    })
    void testAdd(int a, int b, int expected) {
        assertEquals(expected, add(a, b));
    }
}

実行結果:

testAdd(int, int, int) [1] 1, 2, 3 -> PASS
testAdd(int, int, int) [2] 5, 5, 10 -> PASS
testAdd(int, int, int) [3] 10, -3, 7 -> PASS

ここでは、addメソッドのテストを3つの異なる入力値の組み合わせで実行しているよ。

@MethodSourceで複雑なオブジェクトを渡す

もっと複雑なオブジェクトをテストデータとして使いたい場合は、@MethodSourceが便利だよ。スタティックメソッドでStreamなどのコレクションを返すようにすれば、どんなオブジェクトでもテストデータとして使えるんだ。

Java

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.stream.Stream;

class User {
    String name;
    int age;

    User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // ユーザー情報を文字列で返すメソッド
    public String getUserInfo() {
        return name + " (" + age + "歳)";
    }
}

public class UserServiceTest {

    static Stream<User> userProvider() {
        return Stream.of(
            new User("Taro", 30),
            new User("Hanako", 25)
        );
    }

    @ParameterizedTest
    @MethodSource("userProvider")
    void testGetUserInfo(User user) {
        if (user.name.equals("Taro")) {
            assertEquals("Taro (30歳)", user.getUserInfo());
        } else if (user.name.equals("Hanako")) {
            assertEquals("Hanako (25歳)", user.getUserInfo());
        }
    }
}

実行結果:

testGetUserInfo(User) [1] User(name=Taro, age=30) -> PASS
testGetUserInfo(User) [2] User(name=Hanako, age=25) -> PASS

userProviderメソッドでUserオブジェクトのストリームを生成し、それをtestGetUserInfoメソッドで利用しているよ。これで、Userオブジェクトを使ったテストも簡単にできるね。

テストのライフサイクルを制御する!

JUnitでは、テストの実行前後に特定の処理を行いたい場合があるよね。例えば、テストデータを用意したり、テスト後にクリーンアップしたり。そんな時に役立つのが、ライフサイクルアノテーションだよ。

@BeforeEachと@AfterEach

個々のテストメソッドの実行前後に処理を行いたい場合は、@BeforeEach@AfterEachを使うよ。

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;

public class DatabaseTest {
    private String connection;

    @BeforeEach
    void setup() {
        // 各テストの前にデータベース接続をシミュレート
        connection = "Connected to DB";
        System.out.println(connection);
    }

    @AfterEach
    void tearDown() {
        // 各テストの後にデータベース接続をクローズ
        connection = null;
        System.out.println("Disconnected from DB");
    }

    @Test
    void testGetData() {
        assertEquals("Connected to DB", connection);
        System.out.println("Getting data...");
    }

    @Test
    void testInsertData() {
        assertEquals("Connected to DB", connection);
        System.out.println("Inserting data...");
    }
}

実行結果:

Connected to DB
Getting data...
Disconnected from DB
Connected to DB
Inserting data...
Disconnected from DB

setup()メソッドは各@Testメソッドの前に、tearDown()メソッドは各@Testメソッドの後に実行されているのがわかるね。

@BeforeAllと@AfterAll

テストクラス全体の実行前後に一度だけ処理を行いたい場合は、@BeforeAll@AfterAllを使うよ。これらのメソッドはstaticである必要があるから注意してね。

Java

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class ResourceTest {
    private static boolean resourceInitialized = false;

    @BeforeAll
    static void initResource() {
        // テストクラスが実行される前に一度だけリソースを初期化
        resourceInitialized = true;
        System.out.println("Resource initialized.");
    }

    @AfterAll
    static void cleanupResource() {
        // テストクラスの実行が全て終わった後に一度だけリソースをクリーンアップ
        resourceInitialized = false;
        System.out.println("Resource cleaned up.");
    }

    @Test
    void testResourceUsage1() {
        assertTrue(resourceInitialized);
        System.out.println("Using resource 1...");
    }

    @Test
    void testResourceUsage2() {
        assertTrue(resourceInitialized);
        System.out.println("Using resource 2...");
    }
}

実行結果:

Resource initialized.
Using resource 1...
Using resource 2...
Resource cleaned up.

initResource()メソッドはテストクラス内の全てのテストが始まる前に一度だけ、cleanupResource()メソッドは全てのテストが終わった後に一度だけ実行されているね。

例外テストでエラーケースもバッチリ!

メソッドが特定の状況で例外をスローすることを確認するのも、重要なテストの1つだよね。JUnit 5では、assertThrowsメソッドを使って例外テストを行うことができるよ。

Java

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class MyService {
    public int divide(int numerator, int denominator) {
        if (denominator == 0) {
            throw new IllegalArgumentException("Cannot divide by zero!");
        }
        return numerator / denominator;
    }
}

public class MyServiceTest {

    @Test
    void testDivideByZeroThrowsException() {
        MyService service = new MyService();
        // 0で割るとIllegalArgumentExceptionがスローされることを確認
        assertThrows(IllegalArgumentException.class, () -> service.divide(10, 0));
    }
}

実行結果:

testDivideByZeroThrowsException() -> PASS

assertThrowsの第1引数に期待する例外クラス、第2引数に例外をスローする処理をラムダ式で渡すよ。これで、意図した通りに例外がスローされるかを確認できるんだ。

タイムアウトテストで無限ループを防ぐ!

もしテスト対象のコードが無限ループに陥ってしまったら…テストが終わらないなんてことにもなりかねないよね。そんな時はタイムアウトテストが役に立つよ。指定した時間内に処理が終わらない場合にテストを失敗させることができるんだ。

assertTimeoutまたはassertTimeoutPreemptivelyメソッドを使うよ。

Java

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import java.time.Duration;

public class LongRunningProcess {
    public void process() {
        try {
            Thread.sleep(100); // 100ミリ秒かかる処理をシミュレート
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public void infiniteProcess() {
        while (true) {
            // 無限ループ
        }
    }
}

public class LongRunningProcessTest {

    @Test
    void testProcessCompletesWithinTime() {
        LongRunningProcess process = new LongRunningProcess();
        // 200ミリ秒以内に処理が終わることを確認
        assertTimeout(Duration.ofMillis(200), () -> process.process());
    }

    @Test
    void testInfiniteProcessTimesOut() {
        LongRunningProcess process = new LongRunningProcess();
        // 50ミリ秒以内に処理が終わらないことを確認(無限ループなので失敗する)
        assertTimeout(Duration.ofMillis(50), () -> process.infiniteProcess());
    }
}

実行結果 (testInfiniteProcessTimesOutは失敗):

testProcessCompletesWithinTime() -> PASS
testInfiniteProcessTimesOut() -> FAIL (org.junit.platform.commons.JUnitException: Execution timed out after 50 ms)

assertTimeoutは、指定した時間内に処理が完了するかどうかをチェックするよ。assertTimeoutPreemptivelyは、タイムアウトになった瞬間にスレッドを強制終了させるから、より厳密なタイムアウトチェックができるんだ。

まとめ

今回はJUnitの上級テクニックとして、パラメータ化テストライフサイクルアノテーション例外テストタイムアウトテストを紹介したよ。これらの機能を使いこなせば、より堅牢で効率的なテストコードを書けるようになるはずだ。

  • パラメータ化テスト: 複数のテストデータをまとめて実行し、テストコードの重複を減らす。
  • ライフサイクルアノテーション: テストの実行前後に必要な準備やクリーンアップ処理を行う。
  • 例外テスト: 期待通りに例外がスローされることを確認し、エラーハンドリングをテストする。
  • タイムアウトテスト: 処理が指定した時間内に完了するかを確認し、無限ループなどを防ぐ。

これらを活用して、君のテストコードをもっとリッチに、もっとパワフルにしていこう!


前回の記事はこちらから読めるよ!

コメント

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