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());
主な違いのポイント:
観点 | 従来のassert | assertThat |
---|---|---|
文法構造 | メソッド名で判定条件を表現 | 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") ));
可読性向上のポイント:
- 自然な英語表現
is()
,not()
,hasItem()
などのMatcherを使用することで、テストの意図が英語の文章のように読める
- 複数条件の組み合わせ
allOf()
,anyOf()
などを使用して、複数の条件をスッキリと記述できる
- 詳細なエラーメッセージ
// エラーメッセージの例 // 従来の方法 // 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は用途別に以下のようなカテゴリに分類できます:
- 基本的な比較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)); // 以下 }
- コレクション用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")); // 順序不問 }
- 文字列用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()); }
このように、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))); } }
これらの例は、実際のプロジェクトで使用できる実践的なテストパターンを示しています。
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で実現する保守性の高いテストコード
本記事で学んだこと
- assertThatの基本的な特徴
- 従来のassertに比べて高い可読性
- 詳細なエラーメッセージ
- 柔軟な条件指定が可能
- Matcherを活用した直感的な記述
- 実践的な活用のポイント
- コレクション、オブジェクト、例外など様々なケースに対応
- カスタムMatcherによる独自の検証ロジック作成
- 複数条件の組み合わせによる柔軟なテスト
- 各レイヤーに適したテストの書き方
- 保守性を高めるためのプラクティス
- 適切なMatcherの選択
- テストヘルパーメソッドの活用
- テストケースの構造化
- リファクタリング手法の適用
実践のためのNext Steps
- 既存のテストコードの見直し
// 従来のassertをassertThatに置き換える // Before assertEquals(expected, actual); assertTrue(condition); // After assertThat(actual, is(expected)); assertThat(condition, is(true));
- カスタムMatcherの導入
- プロジェクト固有の検証ロジックを抽出
- 再利用可能なMatcherの作成
- テストコードの共通化
- テストコードの整理
@Nested
を使用したテストの構造化- ヘルパーメソッドの作成
- テストデータの集中管理
参考リソース
- 公式ドキュメント
- JUnit 5ユーザーガイド
- Hamcrestドキュメント
- 関連ツール
- AssertJ(代替的なアサーションライブラリ)
- Mockito(モックフレームワーク)
- 推奨書籍
- 「Effective Unit Testing」
- 「xUnit Test Patterns」
assertThatを活用することで、テストコードの品質と保守性を大きく向上させることができます。本記事で紹介した手法を実践し、より良いテストコードを目指してください。
次のステップとして、チーム内でコードレビューを通じてこれらのプラクティスを共有し、プロジェクト全体のテスト品質向上につなげることをお勧めします。