【保守性抜群】JUnit assertThatの使い方完全ガイド:7つの実践的テクニック

assertThatとは?従来のassertとの違いを解説

assertThatが生まれた背景と目的

JUnit 4.4で導入されたassertThatは、より表現力豊かで可読性の高いテストコードを書くために開発されました。従来のassertメソッドには以下のような課題がありました:

課題
  • テストの意図が分かりにくい
  • エラーメッセージが不親切
  • 柔軟な比較が難しい

これらの課題を解決するため、Hamcrest matcherを活用したassertThatが導入され、以下のような利点をもたらしました:

利点
  • 自然言語に近い表現でテストが書ける
  • 詳細なエラーメッセージが自動生成される
  • 柔軟で拡張可能な比較機能

従来のassertとの文法の違いを比較

従来のassertメソッドとassertThatの最も大きな違いは、文法構造にあります。以下で具体的な比較を見てみましょう:

// 従来のassert
assertEquals("expected value", actual);
assertTrue(list.contains(element));
assertNotNull(object);

// assertThatを使用した場合
assertThat(actual, is("expected value"));
assertThat(list, hasItem(element));
assertThat(object, notNullValue());

主な違いのポイント:

観点従来のassertassertThat
文法構造メソッド名で判定条件を表現Matcherで判定条件を表現
引数順序期待値が先、実際値が後実際値が先、Matcherが後
拡張性新しい判定には新メソッドが必要Matcherの組み合わせで対応可能
エラーメッセージ基本的な情報のみより詳細な情報を自動生成

assertThatがもたらす可読性の向上

assertThatの使用により、テストコードの可読性が大きく向上します。具体例で見てみましょう:

// 従来の方法
List<String> fruits = Arrays.asList("apple", "banana", "orange");
assertTrue(fruits.size() == 3);
assertTrue(fruits.contains("apple"));
assertTrue(fruits.contains("banana"));
assertTrue(fruits.contains("orange"));

// assertThatを使用した場合
List<String> fruits = Arrays.asList("apple", "banana", "orange");
assertThat(fruits, allOf(
    hasSize(3),
    hasItems("apple", "banana", "orange")
));

可読性向上のポイント:

  1. 自然な英語表現
    • is(), not(), hasItem()などのMatcherを使用することで、テストの意図が英語の文章のように読める
  2. 複数条件の組み合わせ
    • allOf(), anyOf()などを使用して、複数の条件をスッキリと記述できる
  3. 詳細なエラーメッセージ
   // エラーメッセージの例
   // 従来の方法
   // Expected true but was false

   // assertThatの場合
   // Expected: a collection with size <3> and having items ["apple", "banana", "orange"]
   // but: size was <2>

このように、assertThatを使用することで、テストコードの意図がより明確になり、保守性の高いテストを書くことが可能になります。

assertThatの基本的な使い方を徹底解説

assertThatの基本構文を理解しよう

assertThatの基本構文は以下の形式です:

assertThat(実際の値, Matcher);

具体的な使用例を見てみましょう:

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;

public class BasicAssertThatTest {
    @Test
    void basicAssertionExamples() {
        // 基本的な等価性チェック
        String actual = "test value";
        assertThat(actual, is("test value"));

        // nullチェック
        String nullValue = null;
        assertThat(nullValue, nullValue());

        // 否定のチェック
        assertThat(actual, not("wrong value"));

        // 型のチェック
        assertThat(actual, instanceOf(String.class));
    }
}

主要なMatcherの使い方と使い分け

Matcherは用途別に以下のようなカテゴリに分類できます:

  1. 基本的な比較Matcher
@Test
void basicMatcherExamples() {
    // 等価性の比較
    assertThat("value", is("value"));        // 等しい
    assertThat("value", not("other"));       // 等しくない
    assertThat(null, nullValue());           // nullである
    assertThat("value", notNullValue());     // nullでない

    // 数値の比較
    assertThat(5, greaterThan(3));           // より大きい
    assertThat(5, lessThan(7));              // より小さい
    assertThat(5, greaterThanOrEqualTo(5));  // 以上
    assertThat(5, lessThanOrEqualTo(5));     // 以下
}
  1. コレクション用Matcher
@Test
void collectionMatcherExamples() {
    List<String> fruits = Arrays.asList("apple", "banana", "orange");

    // コレクションの要素チェック
    assertThat(fruits, hasItem("apple"));             // 特定の要素を含む
    assertThat(fruits, hasItems("apple", "banana"));  // 複数の要素を含む
    assertThat(fruits, hasSize(3));                   // サイズのチェック

    // コレクションの状態チェック
    assertThat(fruits, is(empty()));                  // 空のコレクション
    assertThat(fruits, is(not(empty())));            // 空でないコレクション

    // 順序を考慮したチェック
    assertThat(fruits, contains("apple", "banana", "orange"));  // 完全一致(順序含む)
    assertThat(fruits, containsInAnyOrder("banana", "apple", "orange"));  // 順序不問
}
  1. 文字列用Matcher
@Test
void stringMatcherExamples() {
    String text = "Hello, World!";

    // 文字列の部分チェック
    assertThat(text, startsWith("Hello"));    // 前方一致
    assertThat(text, endsWith("World!"));     // 後方一致
    assertThat(text, containsString("lo, W")); // 部分一致

    // 正規表現によるチェック
    assertThat(text, matchesPattern("Hello.*!")); // パターンマッチ

    // 大文字小文字を無視したチェック
    assertThat(text, equalToIgnoringCase("HELLO, WORLD!"));
}

カスタムMatcherの作成方法

独自のMatcherが必要な場合は、以下のように作成できます:

public class IsValidEmail extends TypeSafeMatcher<String> {
    @Override
    protected boolean matchesSafely(String email) {
        // メールアドレスの検証ロジック
        return email != null && 
               email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("a valid email address");
    }

    // ファクトリーメソッド
    public static Matcher<String> isValidEmail() {
        return new IsValidEmail();
    }
}

// 使用例
@Test
void customMatcherExample() {
    String email = "test@example.com";
    assertThat(email, isValidEmail());
}
カスタムMatcherを作成する際の重要なポイント:
  1. TypeSafeMatcherを継承する
    • 型安全なMatcherを作成できる
    • コンパイル時の型チェックが可能
  2. matchesSafelyメソッドの実装
    • 実際のマッチング論理を記述
    • nullチェックなどの基本的な検証は親クラスで実施
  3. describeToメソッドの実装
    • エラーメッセージの生成に使用
    • 期待する状態を明確に記述
  4. ファクトリーメソッドの提供
    • 使用時の可読性向上
    • static importでの使用を可能に

このように、assertThatは基本的な使用方法から、カスタマイズまで柔軟に対応できる強力なツールです。

実践的なassertThatの活用テクニック7選

コレクションに対する柔軟なアサーション

コレクションのテストでは、要素の存在確認だけでなく、順序や条件に応じた検証が必要になります。

@Test
void collectionAssertionExamples() {
    List<User> users = Arrays.asList(
        new User("Alice", 25),
        new User("Bob", 30),
        new User("Charlie", 35)
    );

    // リスト内の特定の条件を満たす要素の検証
    assertThat(users, hasItem(hasProperty("name", is("Alice"))));

    // 複数の条件を組み合わせた検証
    assertThat(users, everyItem(
        hasProperty("age", greaterThan(20))
    ));

    // コレクションの変換結果の検証
    assertThat(
        users.stream()
             .map(User::getName)
             .collect(Collectors.toList()),
        containsInAnyOrder("Charlie", "Bob", "Alice")
    );
}

オブジェクトの部分的な検証方法

オブジェクトの特定のフィールドやプロパティのみを検証する場合の手法です。

@Test
void partialObjectVerificationExamples() {
    Product product = new Product(
        "laptop",
        "Latest model laptop",
        new Price(1000.00, "USD"),
        LocalDate.now()
    );

    // 特定のプロパティの検証
    assertThat(product, allOf(
        hasProperty("name", is("laptop")),
        hasProperty("price", hasProperty("amount", closeTo(1000.00, 0.01)))
    ));

    // ネストされたオブジェクトの検証
    assertThat(product.getPrice(), allOf(
        hasProperty("amount", greaterThan(0.0)),
        hasProperty("currency", is("USD"))
    ));
}

例外処理のテストテクニック

例外のテストでは、例外の型だけでなく、メッセージや原因も含めて検証できます。

@Test
void exceptionTestingExamples() {
    // ExpectedExceptionルール使用
    @Rule
    public ExpectedException thrown = ExpectedException.none();

    // 例外の詳細な検証
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage(containsString("Invalid input"));
    thrown.expectCause(isA(NullPointerException.class));

    // Java 8以降の方法
    assertThatThrownBy(() -> {
        throw new IllegalArgumentException("Invalid input");
    })
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessageContaining("Invalid input");

    // try-catchを使用した方法
    try {
        someMethodThatThrowsException();
        fail("Expected an exception to be thrown");
    } catch (Exception e) {
        assertThat(e, allOf(
            isA(IllegalArgumentException.class),
            hasProperty("message", containsString("Invalid input"))
        ));
    }
}

複数の条件を組み合わせたテスト方法

複数の条件を論理的に組み合わせて、より複雑な検証を行う方法です。

@Test
void combinedConditionsExample() {
    Order order = new Order("ORD001", 150.00, "PENDING", 3);

    // 複数の条件をANDで結合
    assertThat(order, allOf(
        hasProperty("total", greaterThan(100.00)),
        hasProperty("status", is("PENDING")),
        hasProperty("itemCount", lessThan(5))
    ));

    // 複数の条件をORで結合
    assertThat(order.getStatus(), anyOf(
        is("PENDING"),
        is("PROCESSING"),
        is("COMPLETED")
    ));

    // 否定条件との組み合わせ
    assertThat(order, allOf(
        not(hasProperty("total", lessThan(0.0))),
        not(hasProperty("status", isIn(Arrays.asList("CANCELLED", "REJECTED"))))
    ));
}

日付・時刻に関するテストの書き方

日付や時刻の検証には、専用のMatcherを使用すると効果的です。

@Test
void dateTimeTestingExamples() {
    LocalDateTime now = LocalDateTime.now();
    LocalDate today = LocalDate.now();

    // 日付の比較
    assertThat(today, isIn(
        Arrays.asList(
            LocalDate.now(),
            LocalDate.now().plusDays(1)
        )
    ));

    // 時間範囲の検証
    assertThat(now, allOf(
        greaterThan(LocalDateTime.now().minusHours(1)),
        lessThan(LocalDateTime.now().plusHours(1))
    ));

    // カスタムMatcherを使用した日付検証
    assertThat(today, is(withinDays(7, LocalDate.now())));
}

// カスタムMatcher例
public static Matcher<LocalDate> withinDays(int days, LocalDate baseDate) {
    return new TypeSafeMatcher<LocalDate>() {
        @Override
        protected boolean matchesSafely(LocalDate date) {
            return ChronoUnit.DAYS.between(baseDate, date) <= days;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("within " + days + " days of " + baseDate);
        }
    };
}

文字列の高度な検証方法

文字列の検証では、正規表現やパターンマッチングを活用できます。

@Test
void advancedStringValidationExamples() {
    String text = "Hello, World! This is a test message.";

    // 複数のパターンマッチング
    assertThat(text, allOf(
        matchesPattern("^Hello.*"),
        containsString("World"),
        endsWith("message."),
        not(containsString("error"))
    ));

    // 大文字小文字を無視した比較
    assertThat(text, equalToIgnoringCase("HELLO, WORLD! THIS IS A TEST MESSAGE."));

    // 文字列長の検証
    assertThat(text.length(), allOf(
        greaterThan(20),
        lessThan(50)
    ));
}

非同期処理のテストテクニック

非同期処理のテストでは、タイムアウトや完了状態の検証が重要です。

@Test
void asynchronousTestingExamples() throws Exception {
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        // 非同期処理
        try {
            Thread.sleep(1000);
            return "Result";
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });

    // タイムアウトを指定した待機
    String result = future.get(2, TimeUnit.SECONDS);
    assertThat(result, is("Result"));

    // 非同期処理の完了状態の検証
    assertThat(future.isDone(), is(true));

    // 例外のハンドリング
    CompletableFuture<String> failingFuture = CompletableFuture.supplyAsync(() -> {
        throw new RuntimeException("Async error");
    });

    assertThatThrownBy(() -> failingFuture.get())
        .isInstanceOf(ExecutionException.class)
        .hasRootCauseInstanceOf(RuntimeException.class);
}

これらのテクニックを適切に組み合わせることで、より堅牢で保守性の高いテストコードを作成できます。

assertThatを使った実装例と解説

リポジトリレイヤーのテストコード例

リポジトリレイヤーでは、データの永続化に関するテストを行います。

@SpringBootTest
class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    void findByEmailTest() {
        // テストデータのセットアップ
        User user = new User();
        user.setEmail("test@example.com");
        user.setName("Test User");
        user.setActive(true);
        userRepository.save(user);

        // テスト実行と検証
        Optional<User> found = userRepository.findByEmail("test@example.com");

        assertThat(found, allOf(
            is(present()),  // Optionalが値を持っているか
            hasProperty("value", allOf(  // 中身の検証
                hasProperty("email", is("test@example.com")),
                hasProperty("name", is("Test User")),
                hasProperty("active", is(true))
            ))
        ));
    }

    @Test
    void findActiveUsersTest() {
        // テストデータのセットアップ
        User activeUser1 = new User("active1@example.com", "Active 1", true);
        User activeUser2 = new User("active2@example.com", "Active 2", true);
        User inactiveUser = new User("inactive@example.com", "Inactive", false);

        userRepository.saveAll(Arrays.asList(activeUser1, activeUser2, inactiveUser));

        // テスト実行
        List<User> activeUsers = userRepository.findByActiveTrue();

        // 検証
        assertThat(activeUsers, allOf(
            hasSize(2),
            everyItem(hasProperty("active", is(true))),
            containsInAnyOrder(
                hasProperty("email", is("active1@example.com")),
                hasProperty("email", is("active2@example.com"))
            )
        ));
    }
}

サービスレイヤーのテストコード例

サービスレイヤーでは、ビジネスロジックのテストを行います。

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private UserService userService;

    @Test
    void registerUserTest() {
        // テストデータ
        UserRegistrationDto dto = new UserRegistrationDto(
            "new@example.com",
            "New User",
            "password123"
        );

        // モックの設定
        when(userRepository.findByEmail(dto.getEmail()))
            .thenReturn(Optional.empty());
        when(userRepository.save(any(User.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        // テスト実行
        User registered = userService.registerUser(dto);

        // 検証
        assertThat(registered, allOf(
            hasProperty("email", is(dto.getEmail())),
            hasProperty("name", is(dto.getName())),
            hasProperty("active", is(true)),
            hasProperty("password", not(dto.getPassword()))  // パスワードがハッシュ化されていることを確認
        ));

        // メソッド呼び出しの検証
        verify(userRepository).save(any(User.class));
        verify(emailService).sendWelcomeEmail(registered);
    }

    @Test
    void updateUserProfileTest() {
        // テストデータ
        User existingUser = new User("user@example.com", "Old Name", true);
        UserProfileUpdateDto updateDto = new UserProfileUpdateDto(
            "New Name",
            "new-password",
            true
        );

        // モックの設定
        when(userRepository.findById(1L))
            .thenReturn(Optional.of(existingUser));
        when(userRepository.save(any(User.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));

        // テスト実行
        User updated = userService.updateProfile(1L, updateDto);

        // 検証
        assertThat(updated, allOf(
            hasProperty("name", is(updateDto.getName())),
            hasProperty("email", is(existingUser.getEmail())),  // メールアドレスは変更されないことを確認
            hasProperty("active", is(updateDto.isActive()))
        ));
    }
}

コントローラーレイヤーのテストコード例

コントローラーレイヤーでは、HTTPリクエスト/レスポンスのテストを行います。

@WebMvcTest(UserController.class)
class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void createUserTest() throws Exception {
        // テストデータ
        UserRegistrationDto dto = new UserRegistrationDto(
            "new@example.com",
            "New User",
            "password123"
        );

        User createdUser = new User();
        createdUser.setId(1L);
        createdUser.setEmail(dto.getEmail());
        createdUser.setName(dto.getName());
        createdUser.setActive(true);

        // モックの設定
        when(userService.registerUser(any(UserRegistrationDto.class)))
            .thenReturn(createdUser);

        // テスト実行と検証
        MvcResult result = mockMvc.perform(post("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(dto)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id", is(1)))
            .andExpect(jsonPath("$.email", is(dto.getEmail())))
            .andExpect(jsonPath("$.name", is(dto.getName())))
            .andExpect(jsonPath("$.active", is(true)))
            .andReturn();

        // レスポンスの詳細な検証
        String responseJson = result.getResponse().getContentAsString();
        UserResponse response = objectMapper.readValue(responseJson, UserResponse.class);

        assertThat(response, allOf(
            hasProperty("id", is(1L)),
            hasProperty("email", is(dto.getEmail())),
            hasProperty("name", is(dto.getName())),
            hasProperty("active", is(true)),
            not(hasProperty("password"))  // パスワードが含まれていないことを確認
        ));
    }

    @Test
    void getUsersTest() throws Exception {
        // テストデータ
        List<User> users = Arrays.asList(
            new User("user1@example.com", "User 1", true),
            new User("user2@example.com", "User 2", true)
        );

        // モックの設定
        when(userService.getAllUsers(any(Pageable.class)))
            .thenReturn(new PageImpl<>(users));

        // テスト実行と検証
        mockMvc.perform(get("/api/users")
            .param("page", "0")
            .param("size", "10"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.content", hasSize(2)))
            .andExpect(jsonPath("$.content[0].email", is("user1@example.com")))
            .andExpect(jsonPath("$.content[1].email", is("user2@example.com")))
            .andExpect(jsonPath("$.totalElements", is(2)));
    }
}
各レイヤーのテストポイント:
  1. リポジトリテスト
    • データの永続化
    • クエリの正確性
    • トランザクション管理
  2. サービステスト
    • ビジネスロジックの正確性
    • 外部サービスとの連携
    • 例外ハンドリング
  3. コントローラーテスト
    • リクエスト/レスポンスのバリデーション
    • HTTPステータスコード
    • レスポンスボディの構造

これらの例は、実際のプロジェクトで使用できる実践的なテストパターンを示しています。

assertThatのベストプラクティスとアンチパターン

テストの読みやすさを向上させるTips

1. 適切なMatcher選択

// 悪い例:汎用的なMatcherの過剰使用
assertThat(user.getAge(), is(greaterThan(20)));
assertThat(user.getName(), is(not(nullValue())));

// 良い例:目的に適したMatcherの使用
assertThat(user.getAge(), greaterThan(20));
assertThat(user.getName(), notNullValue());

2. カスタムMatcherの活用

// カスタムMatcherの定義
public class UserMatchers {
    public static Matcher<User> isValidUser() {
        return allOf(
            hasProperty("age", greaterThan(0)),
            hasProperty("name", not(isEmptyOrNullString())),
            hasProperty("email", matchesPattern(".+@.+\\..+"))
        );
    }
}

// テストでの使用
@Test
void userValidationTest() {
    User user = new User("John", 25, "john@example.com");
    assertThat(user, isValidUser());
}

3. テストメソッド名の命名規則

// 悪い例:抽象的な名前
@Test
void test1() { ... }

// 良い例:テストの意図が明確な名前
@Test
void shouldReturnUserWhenValidEmailIsProvided() { ... }

よくある間違いとその回避方法

1. 過剰なアサーション

// 悪い例:一つのテストで多すぎる検証
@Test
void userRegistrationTest() {
    User user = userService.register(userDto);
    assertThat(user.getId(), notNullValue());
    assertThat(user.getName(), is("John"));
    assertThat(user.getEmail(), is("john@example.com"));
    assertThat(user.getAge(), is(25));
    assertThat(user.getCreatedAt(), is(notNullValue()));
    assertThat(user.getUpdatedAt(), is(notNullValue()));
    assertThat(user.getStatus(), is("ACTIVE"));
}

// 良い例:関連する検証をグループ化
@Test
void userRegistrationTest() {
    User user = userService.register(userDto);

    assertThat(user, allOf(
        hasProperty("id", notNullValue()),
        hasProperty("name", is("John")),
        hasProperty("email", is("john@example.com")),
        hasProperty("age", is(25))
    ));

    // 監査フィールドの検証は別のテストケースに分離
    assertAuditFields(user);
}

@Test
void shouldSetCorrectAuditFieldsOnRegistration() {
    User user = userService.register(userDto);
    assertAuditFields(user);
}

private void assertAuditFields(User user) {
    assertThat(user, allOf(
        hasProperty("createdAt", notNullValue()),
        hasProperty("updatedAt", notNullValue()),
        hasProperty("status", is("ACTIVE"))
    ));
}

2. 不適切なMatcherの使用

// 悪い例:数値の比較に不適切なMatcherを使用
assertThat(calculation.getTotal(), is(100.0));  // 浮動小数点の厳密な比較

// 良い例:適切なMatcherの使用
assertThat(calculation.getTotal(), closeTo(100.0, 0.001));  // 許容誤差を考慮

3. テストデータの重複

// 悪い例:テストデータの重複
@Test
void test1() {
    User user = new User("John", 25, "john@example.com");
    // テスト実行
}

@Test
void test2() {
    User user = new User("John", 25, "john@example.com");
    // テスト実行
}

// 良い例:テストデータの集中管理
class UserTestData {
    static final User VALID_USER = new User("John", 25, "john@example.com");
    static User createUserWithAge(int age) {
        return new User("John", age, "john@example.com");
    }
}

@Test
void test1() {
    User user = UserTestData.VALID_USER;
    // テスト実行
}

@Test
void test2() {
    User user = UserTestData.createUserWithAge(30);
    // テスト実行
}

テストコードのリファクタリング手法

1. テストヘルパーメソッドの作成

// リファクタリング前
@Test
void orderProcessingTest() {
    Order order = new Order();
    order.setItems(Arrays.asList(
        new OrderItem("item1", 100),
        new OrderItem("item2", 200)
    ));
    order.setStatus("NEW");

    Order processedOrder = orderService.process(order);

    assertThat(processedOrder.getStatus(), is("PROCESSED"));
    assertThat(processedOrder.getTotalAmount(), is(300));
    assertThat(processedOrder.getItems(), hasSize(2));
}

// リファクタリング後
@Test
void orderProcessingTest() {
    Order order = createOrderWithItems(
        createOrderItem("item1", 100),
        createOrderItem("item2", 200)
    );

    Order processedOrder = orderService.process(order);

    assertOrderIsProcessedCorrectly(processedOrder, 300, 2);
}

private Order createOrderWithItems(OrderItem... items) {
    Order order = new Order();
    order.setItems(Arrays.asList(items));
    order.setStatus("NEW");
    return order;
}

private OrderItem createOrderItem(String name, int price) {
    return new OrderItem(name, price);
}

private void assertOrderIsProcessedCorrectly(Order order, int expectedTotal, int expectedItemCount) {
    assertThat(order, allOf(
        hasProperty("status", is("PROCESSED")),
        hasProperty("totalAmount", is(expectedTotal)),
        hasProperty("items", hasSize(expectedItemCount))
    ));
}

2. テストフィクスチャーの改善

// リファクタリング前
class UserServiceTest {
    private UserService userService;
    private UserRepository userRepository;
    private EmailService emailService;

    @BeforeEach
    void setUp() {
        userRepository = mock(UserRepository.class);
        emailService = mock(EmailService.class);
        userService = new UserService(userRepository, emailService);
    }
}

// リファクタリング後
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private UserService userService;

    // テストフィクスチャーの共通設定
    @BeforeEach
    void setUp() {
        // 共通のモック設定のみを記述
        when(userRepository.findByEmail(anyString()))
            .thenReturn(Optional.empty());
    }
}

3. テストケースの構造化

// リファクタリング前
class UserServiceTest {
    @Test
    void test1() { ... }
    @Test
    void test2() { ... }
    @Test
    void test3() { ... }
}

// リファクタリング後
class UserServiceTest {
    @Nested
    class WhenCreatingNewUser {
        @Test
        void shouldCreateUserSuccessfully() { ... }

        @Test
        void shouldThrowExceptionForDuplicateEmail() { ... }
    }

    @Nested
    class WhenUpdatingUser {
        @Test
        void shouldUpdateUserDetailsSuccessfully() { ... }

        @Test
        void shouldThrowExceptionForNonExistentUser() { ... }
    }
}

これらのベストプラクティスとリファクタリング手法を適用することで、より保守性が高く、理解しやすいテストコードを作成できます。

まとめ:assertThatで実現する保守性の高いテストコード

本記事で学んだこと

  1. assertThatの基本的な特徴
    • 従来のassertに比べて高い可読性
    • 詳細なエラーメッセージ
    • 柔軟な条件指定が可能
    • Matcherを活用した直感的な記述
  2. 実践的な活用のポイント
    • コレクション、オブジェクト、例外など様々なケースに対応
    • カスタムMatcherによる独自の検証ロジック作成
    • 複数条件の組み合わせによる柔軟なテスト
    • 各レイヤーに適したテストの書き方
  3. 保守性を高めるためのプラクティス
    • 適切なMatcherの選択
    • テストヘルパーメソッドの活用
    • テストケースの構造化
    • リファクタリング手法の適用

実践のためのNext Steps

  1. 既存のテストコードの見直し
   // 従来のassertをassertThatに置き換える
   // Before
   assertEquals(expected, actual);
   assertTrue(condition);

   // After
   assertThat(actual, is(expected));
   assertThat(condition, is(true));
  1. カスタムMatcherの導入
    • プロジェクト固有の検証ロジックを抽出
    • 再利用可能なMatcherの作成
    • テストコードの共通化
  2. テストコードの整理
    • @Nestedを使用したテストの構造化
    • ヘルパーメソッドの作成
    • テストデータの集中管理

参考リソース

  1. 公式ドキュメント
    • JUnit 5ユーザーガイド
    • Hamcrestドキュメント
  2. 関連ツール
    • AssertJ(代替的なアサーションライブラリ)
    • Mockito(モックフレームワーク)
  3. 推奨書籍
    • 「Effective Unit Testing」
    • 「xUnit Test Patterns」

assertThatを活用することで、テストコードの品質と保守性を大きく向上させることができます。本記事で紹介した手法を実践し、より良いテストコードを目指してください。

次のステップとして、チーム内でコードレビューを通じてこれらのプラクティスを共有し、プロジェクト全体のテスト品質向上につなげることをお勧めします。