はじめに
本記事では、JUnitのアサーションやテストライフサイクル、パラメータ化テストといった基本機能の活用方法について説明していきます。
JUnitの基礎については前回記事で説明していますので、そちらをご参考ください。
JUnitの基本① 基礎知識~簡単なテストメソッドの作成
対象者
- Javaの基礎知識を持っている方
- JUnitに初めて触れる方、もしくは少し触れたことのある方
動作環境
- Eclipse 2022(Pleiades All in One)
事前準備
前回記事を参照ください。
JUnitの基本① 基礎知識~簡単なテストメソッドの作成-事前準備
アサーション
前回記事で紹介した基本的なアサーションメソッドについて、実際の使用例を交えて説明します。
この他にも複数のアサーションがありますので、ユーザーガイドやAPIドキュメントを参照ください。
JUnit 5 ユーザーガイド
JUnit5 API ドキュメント-Class Assertions
今回、説明に用いるテスト対象クラスはこちらです。前回記事で使用したものに処理を追加しています。
package junit.demo;
public class User {
private String name;
private int age;
private String address;
public User(String name, int age, String address) {
validateName(name);
validateAge(age);
this.name = name;
this.age = age;
this.address = address;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getAddress() {
return address;
}
public void setName(String name) {
validateName(name);
this.name = name;
}
public void setAge(int age) {
validateAge(age);
this.age = age;
}
public void setAddress(String address) {
if (address == null || address.isEmpty()) {
throw new IllegalArgumentException("住所は空であってはなりません");
}
this.address = address;
}
public boolean isAdult() {
return age >= 18; // 成人かどうかを判定
}
// プライベートメソッド(名前の検証)
private void validateName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("名前は空であってはなりません");
}
}
// プライベートメソッド(年齢の検証)
private void validateAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年齢は0以上である必要があります");
}
}
}
テストクラスは以下になります。
※アサーションメソッドの使用例の紹介のため、通常では不要なものも記述しています。
package junit.demo;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class UserTest {
// 正常なユーザーの生成をテスト
@Test
void testUserInitialization() {
User user = new User("Alice", 25, "東京都中央区");
assertEquals("Alice", user.getName()); // Nameが一致していることの検証
assertEquals(25, user.getAge()); // Ageが一致していることの検証
assertNotEquals("Alic", user.getName()); // Nameが異なることの検証
assertNotEquals(26, user.getAge()); // Ageが異なることの検証
}
// セッターを使った更新テスト
@Test
void testUpdateName() {
User user = new User("Alice", 25, "東京都中央区");
user.setName("David");
assertEquals("David", user.getName()); // 更新したNameが一致していることの検証
}
@Test
void testUpdateAge() {
User user = new User("Alice", 25, "東京都中央区");
user.setAge(26);
assertEquals(26, user.getAge()); // 更新したAgeが一致していることの検証
}
@Test
void testUpdateAddress() {
User user = new User("Alice", 25, "東京都中央区");
user.setAddress("東京都千代田区");
assertEquals("東京都千代田区", user.getAddress()); // 更新したAddressが一致していることの検証
}
// 住所がnullのユーザーを作成して更新するテスト
@Test
void testUserInitializationWithNullAddress() {
User user = new User("Alice", 25, null);
assertNull(user.getAddress()); // Addressがnullであることの検証
user.setAddress("東京都中央区");
assertNotNull(user.getAddress()); // 更新したAddressがnullでないことの検証
}
// 成人かどうかの判定ロジックをテスト
@Test
void testIsAdult() {
User adultUser = new User("Bob", 18, "神奈川県横浜市");
User minorUser = new User("Carol", 17, "大阪府大阪市");
assertTrue(adultUser.isAdult()); // 18歳は成人なのでTrueと判定されることの検証
assertFalse(minorUser.isAdult()); // 17歳は成人ではないのでFalseと判定されることの検証
}
// バリデーション機能のテスト
@Test
void testInvalidName() {
assertThrows(IllegalArgumentException.class, () -> new User("", 25, "東京都中央区")); // 名前が空のユーザーを生成した結果、指定した例外がスローされることの検証
assertThrows(IllegalArgumentException.class, () -> new User(null, 25, "東京都中央区")); // 名前がnullのユーザーを生成した結果、指定した例外がスローされることの検証
}
@Test
void testInvalidAge() {
assertThrows(IllegalArgumentException.class, () -> new User("Alice", -1, "東京都中央区")); // 年齢が0未満のユーザーを生成した結果、指定した例外がスローされることの検証
}
// バリデーション機能のテスト2
@Test
void testInvalidName2() {
try {
new User("", 25, "東京都中央区");
fail(); // 直前の処理は失敗する想定だが、もし通ってしまった場合ここで失敗させる
} catch (IllegalArgumentException e) {
assertEquals("名前は空であってはなりません", e.getMessage()); // キャッチしたエラーメッセージが一致することの検証
}
}
// その他
@Test
void testOther() {
User user = new User("Alice", 25, "東京都中央区");
assertAll("テスト失敗",
() -> assertEquals("Alice", user.getName()),
() -> assertEquals(25, user.getAge()),
() -> assertEquals("東京都中央区", user.getAddress())); // 複数のテストを実行でき、1つのテストに失敗しても全てのテストが実行される
}
}
最後のtestOther()メソッドについて、テストが失敗する期待値を準備して実行結果を確認してみましょう。
※名前を"Alic"、住所を"東京都千代田区"に変更し、一致するか検証
// その他
@Test
void testOther() {
User user = new User("Alice", 25, "東京都中央区");
assertAll("テスト失敗",
() -> assertEquals("Alic", user.getName()),
() -> assertEquals(25, user.getAge()),
() -> assertEquals("東京都千代田区", user.getAddress())); // 複数のテストを実行でき、1つのテストに失敗しても全てのテストが実行される
}
testOther()メソッドのJUnitテストを実行すると異常終了し、"テスト失敗"のエラーメッセージと、2件失敗し、それぞれの期待値および実測値が障害トレースに表示されています。
テストケースの構造化
グループ化したいテストメソッドをクラスで囲い、@Nestedアノテーションを付与することで、テストクラスを構造化することができます。
これにより、テストコードの見通しが良くなり、論理的なまとまりができるため、変更や追加が容易になります。
先程のテストクラスに@Nestedアノテーションを付与してみます。
※長くなるため、「バリデーション機能のテスト2」と「その他」は省いています。
package junit.demo;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class UserTest {
@Nested
class UserInitializationTests {
// 正常なユーザーの生成をテスト
@Test
void testUserInitialization() {
User user = new User("Alice", 25, "東京都中央区");
assertEquals("Alice", user.getName()); // Nameが一致していることの検証
assertEquals(25, user.getAge()); // Ageが一致していることの検証
assertNotEquals("Alic", user.getName()); // Nameが異なることの検証
assertNotEquals(26, user.getAge()); // Ageが異なることの検証
}
// 住所がnullのユーザーを作成して更新するテスト
@Test
void testUserInitializationWithNullAddress() {
User user = new User("Alice", 25, null);
assertNull(user.getAddress()); // Addressがnullであることの検証
user.setAddress("東京都中央区");
assertNotNull(user.getAddress()); // 更新したAddressがnullでないことの検証
}
}
@Nested
class UpdateMethodTests {
// セッターを使った更新テスト
@Test
void testUpdateName() {
User user = new User("Alice", 25, "東京都中央区");
user.setName("David");
assertEquals("David", user.getName()); // 更新したNameが一致していることの検証
}
@Test
void testUpdateAge() {
User user = new User("Alice", 25, "東京都中央区");
user.setAge(26);
assertEquals(26, user.getAge()); // 更新したAgeが一致していることの検証
}
@Test
void testUpdateAddress() {
User user = new User("Alice", 25, "東京都中央区");
user.setAddress("東京都千代田区");
assertEquals("東京都千代田区", user.getAddress()); // 更新したAddressが一致していることの検証
}
}
@Nested
class IsAdultTests {
// 成人かどうかの判定ロジックをテスト
@Test
void testIsAdult() {
User adultUser = new User("Bob", 18, "神奈川県横浜市");
User minorUser = new User("Carol", 17, "大阪府大阪市");
assertTrue(adultUser.isAdult()); // 18歳は成人なのでTrueと判定されることの検証
assertFalse(minorUser.isAdult()); // 17歳は成人ではないのでFalseと判定されることの検証
}
}
@Nested
class ValidationTests {
// バリデーション機能のテスト
@Test
void testInvalidName() {
assertThrows(IllegalArgumentException.class, () -> new User("", 25, "東京都中央区")); // 名前が空のユーザーを生成した結果、指定した例外がスローされることの検証
assertThrows(IllegalArgumentException.class, () -> new User(null, 25, "東京都中央区")); // 名前がnullのユーザーを生成した結果、指定した例外がスローされることの検証
}
@Test
void testInvalidAge() {
assertThrows(IllegalArgumentException.class, () -> new User("Alice", -1, "東京都中央区")); // 年齢が0未満のユーザーを生成した結果、指定した例外がスローされることの検証
}
}
}
実行すると、@Nestedアノテーションを付与したグループごとに階層化して表示されます。
※特定のグループのみテスト実行することも可能
カテゴリ化
テストクラスやメソッドに@Tagアノテーションを付与することで、テスト実行時にフィルタリングすることができます。
同様に、先程のテストクラスに@Tagアノテーションを付与してみます。
また、@Tagsアノテーションを使用することで、複数のTagを付与することも可能です。
package junit.demo;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Tags;
import org.junit.jupiter.api.Test;
class UserTest {
@Nested // ユーザー情報の初期化に関するテスト
@Tag("initialization") // 初期化に関するテストのタグ
class UserInitializationTests {
// 正常なユーザーの生成をテスト
@Test
void testUserInitialization() {
User user = new User("Alice", 25, "東京都中央区");
assertEquals("Alice", user.getName()); // Nameが一致していることの検証
assertEquals(25, user.getAge()); // Ageが一致していることの検証
assertNotEquals("Alic", user.getName()); // Nameが異なることの検証
assertNotEquals(26, user.getAge()); // Ageが異なることの検証
}
// 住所がnullのユーザーを作成して更新するテスト
@Test
void testUserInitializationWithNullAddress() {
User user = new User("Alice", 25, null);
assertNull(user.getAddress()); // Addressがnullであることの検証
user.setAddress("東京都中央区");
assertNotNull(user.getAddress()); // 更新したAddressがnullでないことの検証
}
}
@Nested // ユーザー情報の更新に関するテスト
@Tag("update") // 更新に関するテストのタグ
class UpdateMethodTests {
// セッターを使った更新テスト
@Test
void testUpdateName() {
User user = new User("Alice", 25, "東京都中央区");
user.setName("David");
assertEquals("David", user.getName()); // 更新したNameが一致していることの検証
}
@Test
void testUpdateAge() {
User user = new User("Alice", 25, "東京都中央区");
user.setAge(26);
assertEquals(26, user.getAge()); // 更新したAgeが一致していることの検証
}
@Test
void testUpdateAddress() {
User user = new User("Alice", 25, "東京都中央区");
user.setAddress("東京都千代田区");
assertEquals("東京都千代田区", user.getAddress()); // 更新したAddressが一致していることの検証
}
}
@Nested // 成人かどうかの判定に関するテスト
@Tags({
@Tag("logic"), // ロジックに関するテストのタグ
@Tag("boundary-value") // 境界値のテストのタグ
})
class IsAdultTests {
// 成人かどうかの判定ロジックをテスト
@Test
void testIsAdult() {
User adultUser = new User("Bob", 18, "神奈川県横浜市");
User minorUser = new User("Carol", 17, "大阪府大阪市");
assertTrue(adultUser.isAdult()); // 18歳は成人なのでTrueと判定されることの検証
assertFalse(minorUser.isAdult()); // 17歳は成人ではないのでFalseと判定されることの検証
}
}
@Nested // バリデーションに関するテスト
@Tag("validation") // バリデーションに関するテストのタグ
class ValidationTests {
// バリデーション機能のテスト
@Test
void testInvalidName() {
assertThrows(IllegalArgumentException.class, () -> new User("", 25, "東京都中央区")); // 名前が空のユーザーを生成した結果、指定した例外がスローされることの検証
assertThrows(IllegalArgumentException.class, () -> new User(null, 25, "東京都中央区")); // 名前がnullのユーザーを生成した結果、指定した例外がスローされることの検証
}
@Test
@Tag("boundary-value") // 境界値のテストのタグ
void testInvalidAge() {
assertThrows(IllegalArgumentException.class, () -> new User("Alice", -1, "東京都中央区")); // 年齢が0未満のユーザーを生成した結果、指定した例外がスローされることの検証
}
}
}
フィルタリングを行う場合は、テストクラス上で右クリックし、[実行]-[実行の構成]を押下します。
続いて、実行構成画面でタグの包含/除外から構成を押下します。
Include Tagsを選択することで該当するタグが付与されたテストのみを実行することができ、反対にExclude Tagsを選択することで該当するタグが付与されたテストメソッドのみ除外することができます。
今回はInclude Tagsを選択して、"boundary-value"を入力します。
適用ボタンを押下し、実行ボタンを押下することでテストが実行されます。
実行すると、@Tagアノテーションで"boundary-value"を付与したテストのみが実行されました。
共通フィクスチャ
共通フィクスチャとは、複数のテストケース間で共通して使用されるオブジェクトや環境のセットアップを指し、複数のテストケース間で再利用可能な環境を構築し、重複したコードを排除することを目的としています。(テストのライフサイクルの管理)
通常、@BeforeAllや@BeforeEach、 @AfterEachや@AfterAllアノテーションを使用して設定を行います。
@BeforeAll
テストクラス全体でテストが実行される前に一度だけ実行されるため、主にテスト全体で共有するリソースの初期化に使用されます。
(例:データベース接続の設定、外部ライブラリの初期化など)
@BeforeEach
各テストメソッドが実行される前に毎回実行されるため、主に各テストに必要な初期化処理に使用されます。
(例:テストデータの初期化、モックオブジェクトの準備など)
前述のテストクラスでは各テストメソッド内でユーザーを生成しているため、共通化できる部分は@BeforeEachで抜き出してみます。
package junit.demo;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Tags;
import org.junit.jupiter.api.Test;
class UserTest {
User user;
@BeforeEach // 共通フィクスチャ(ユーザーのセットアップ)
public void setUp() {
user = new User("Alice", 25, "東京都中央区");
}
@Nested // ユーザー情報の初期化に関するテスト
@Tag("initialization") // 初期化に関するテストのタグ
class UserInitializationTests {
// 正常なユーザーの生成をテスト
@Test
void testUserInitialization() {
assertEquals("Alice", user.getName()); // Nameが一致していることの検証
assertEquals(25, user.getAge()); // Ageが一致していることの検証
assertNotEquals("Alic", user.getName()); // Nameが異なることの検証
assertNotEquals(26, user.getAge()); // Ageが異なることの検証
}
// 住所がnullのユーザーを作成して更新するテスト
@Test
void testUserInitializationWithNullAddress() {
User user2 = new User("Alice", 25, null); // カスタムユーザー
assertNull(user2.getAddress()); // Addressがnullであることの検証
user2.setAddress("東京都中央区");
assertNotNull(user2.getAddress()); // 更新したAddressがnullでないことの検証
}
}
@Nested // ユーザー情報の更新に関するテスト
@Tag("update") // 更新に関するテストのタグ
class UpdateMethodTests {
// セッターを使った更新テスト
@Test
void testUpdateName() {
user.setName("David");
assertEquals("David", user.getName()); // 更新したNameが一致していることの検証
}
@Test
void testUpdateAge() {
user.setAge(26);
assertEquals(26, user.getAge()); // 更新したAgeが一致していることの検証
}
@Test
void testUpdateAddress() {
user.setAddress("東京都千代田区");
assertEquals("東京都千代田区", user.getAddress()); // 更新したAddressが一致していることの検証
}
}
@Nested // 成人かどうかの判定に関するテスト
@Tags({
@Tag("logic"), // ロジックに関するテストのタグ
@Tag("boundary-value") // 境界値のテストのタグ
})
class IsAdultTests {
// 成人かどうかの判定ロジックをテスト
@Test
void testIsAdult() {
User adultUser = new User("Bob", 18, "神奈川県横浜市"); // カスタムユーザー
User minorUser = new User("Carol", 17, "大阪府大阪市"); // カスタムユーザー
assertTrue(adultUser.isAdult()); // 18歳は成人なのでTrueと判定されることの検証
assertFalse(minorUser.isAdult()); // 17歳は成人ではないのでFalseと判定されることの検証
}
}
@Nested // バリデーションに関するテスト
@Tag("validation") // バリデーションに関するテストのタグ
class ValidationTests {
// バリデーション機能のテスト
@Test
void testInvalidName() {
assertThrows(IllegalArgumentException.class, () -> new User("", 25, "東京都中央区")); // 名前が空のユーザーを生成した結果、指定した例外がスローされることの検証
assertThrows(IllegalArgumentException.class, () -> new User(null, 25, "東京都中央区")); // 名前がnullのユーザーを生成した結果、指定した例外がスローされることの検証
}
@Test
@Tag("boundary-value") // 境界値のテストのタグ
void testInvalidAge() {
assertThrows(IllegalArgumentException.class, () -> new User("Alice", -1, "東京都中央区")); // 年齢が0未満のユーザーを生成した結果、指定した例外がスローされることの検証
}
}
}
@AfterEach
各テストメソッドが実行された後に毎回実行されるため、主に各テストで使用したリソースの解放や後処理に使用されます。
(例:一時ファイルの削除、モックのリセットなど)
@AfterAll
テストクラス全体でテストが実行された後に一度だけ実行されるため、主にリソースのクリーンアップや後始末に使用されます。
(例:データベース接続の解放、外部サービスの停止など)
事前条件
事前条件とは、特定のテストケースを実行するために満たされている必要がある条件を指し、assumeTrueやassumingThatを使用します。
assumeTrueは、指定した条件が真(true)の場合にのみテストを実行し、条件が満たされない場合は、そのテストをスキップします。
assumingThatは、特定の条件が真(true)の場合にのみ、スコープ内(ラムダ式内)の処理を実行します。スコープ外のテストコードは常に実行されます。
assumeTrue
assumeTrueの活用として、testUserInitializationWithNullAddress()メソッド内にwindows環境でのみ実行されるよう処理を追加し、インナークラス:UserInitializationTestsをテスト実行してみます。
~~~
// 住所がnullのユーザーを作成して更新するテスト
@Test
void testUserInitializationWithNullAddress() {
String osName = System.getProperty("os.name").toLowerCase();
assumeTrue(osName.contains("windows"), "このテストはwindows環境でのみ実行されます"); // テストを特定のOSでのみ実行
User user2 = new User("Alice", 25, null); // カスタムユーザー
assertNull(user2.getAddress()); // Addressがnullであることの検証
user2.setAddress("東京都中央区");
assertNotNull(user2.getAddress()); // 更新したAddressがnullでないことの検証
}
~~~
今度は、Linux環境でのみ実行されるように処理を修正してみます。
~~~
assumeTrue(osName.contains("linux"), "このテストはlinux環境でのみ実行されます"); // テストを特定のOSでのみ実行
~~~
実行すると1件がスキップされ、障害トレースにメッセージが出力されました。
assumingThat
次にassumingThatの活用として、testUserInitialization()メソッド内にwindows環境でのみ追加で実行する処理を追加し、インナークラス:UserInitializationTestsをテスト実行してみます。
~~~
// 正常なユーザーの生成をテスト
@Test
void testUserInitialization() {
assertEquals("Alice", user.getName()); // Nameが一致していることの検証
assertEquals(25, user.getAge()); // Ageが一致していることの検証
assertNotEquals("Alic", user.getName()); // Nameが異なることの検証
assertNotEquals(26, user.getAge()); // Ageが異なることの検証
String osName = System.getProperty("os.name").toLowerCase();
assumingThat(osName.contains("windows"), () -> { // 特定のOSでのみ追加で処理を実行
System.out.println("(1)テスト実行");
});
System.out.println("(2)テスト終了");
}
~~~
実行すると、コンソールに(1)テスト実行、(2)テスト終了の両方が出力されました。
今度は、Linux環境でのみ追加で処理が実行されるように修正してみます。
~~~
assumingThat(osName.contains("linux"), () -> { // 特定のOSでのみ追加で処理を実行
~~~
実行すると、コンソールに(2)テスト終了のみ出力されました。
@Disabled
@Disabledアノテーションについてもここで触れておきます。
テストクラスやメソッドに@Disabledを付与することで、そのテストを無効化することができます。
testUserInitialization()メソッドに@Disabledを付与して実行してみましょう。
~~~
// 正常なユーザーの生成をテスト
@Disabled
@Test
void testUserInitialization() {
~~~
実行すると、testUserInitialization()メソッドのみスキップされました。
パラメータ化テスト
@Testの代わりに@ParameterizedTest付与することで、パラメータ化テストを宣言することができます。
これにより、テストデータをメソッドの外に出し、引数としてテストメソッドに渡すことが可能となります。
引数のタイプとして指定できるアノテーションは以下の2つがあります。
@ValueSource
String、int、long、double等の配列を指定し、配列の前から順番にテストを行います。
例)
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5}) // テスト対象の値を指定
void testWithIntValues(int number) {
assertTrue(number > 0); // 全ての値が正の整数かを検証
}
@CsvSource
カンマ区切り文字列の配列を指定し、配列の前から順番にテストを行います。
例として、Userクラスに新たにメソッドを追加し、@CsvSourceを用いてJUnitテストを実行してみます。
~~~
public boolean checkSeniorBonus() {
return age >= 60 && address.equals("東京都"); // 東京都の60歳以上か判定(インスタンス変数を使用)
}
~~~
~~~
@Nested // 年齢と住所による判定に関するテスト
@Tags({
@Tag("logic"), // ロジックに関するテストのタグ
@Tag("boundary-value") // 境界値のテストのタグ
})
class CheckSeniorBonusTest {
// 東京都の60歳以上かの判定ロジックをテスト
@ParameterizedTest
@CsvSource({"60, '東京都', true",
"60, '神奈川県', false",
"59, '東京都', false"
})
void testCheckSeniorBonus(int age, String address, boolean expected) {
User seniorUser = new User("test", age, address);
boolean actual = seniorUser.checkSeniorBonus();
assertEquals(expected, actual);
}
}
~~~
テストケースとしては、年齢&住所ともに条件を満たしてtrueとなるケースと、どちらかが条件を満たさずにfalseとなるケースの3ケースを用意しています。
実行すると、以下のように3ケースが実行され、いずれも期待値通りのため正常終了していることが分かります。
おわりに
今回の記事では、JUnitのアサーションやテストライフサイクル、パラメータ化テストといった基本機能の活用方法について説明してきました。
次回の記事では、モックやスタブ等を用いたテスト手法について説明していますので、良ければそちらもご覧ください。
JUnitの基本③ 様々なテスト手法