前回(■ – 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の上級テクニックとして、パラメータ化テスト、ライフサイクルアノテーション、例外テスト、タイムアウトテストを紹介したよ。これらの機能を使いこなせば、より堅牢で効率的なテストコードを書けるようになるはずだ。
- パラメータ化テスト: 複数のテストデータをまとめて実行し、テストコードの重複を減らす。
- ライフサイクルアノテーション: テストの実行前後に必要な準備やクリーンアップ処理を行う。
- 例外テスト: 期待通りに例外がスローされることを確認し、エラーハンドリングをテストする。
- タイムアウトテスト: 処理が指定した時間内に完了するかを確認し、無限ループなどを防ぐ。
これらを活用して、君のテストコードをもっとリッチに、もっとパワフルにしていこう!
前回の記事はこちらから読めるよ!


コメント