テスト方法
Spring Bootでのテスト方法
Section titled “Spring Bootでのテスト方法”Spring Bootアプリケーションの包括的なテスト戦略について詳しく解説します。
テストピラミッド
Section titled “テストピラミッド”テストは、以下の3つの層に分けて実装します:
/\ / \ E2Eテスト(少数) /____\ / \ 統合テスト(中程度) /________\ / \ ユニットテスト(多数) /____________\ユニットテスト
Section titled “ユニットテスト”サービス層のテスト
Section titled “サービス層のテスト”@ExtendWith(MockitoExtension.class)class UserServiceTest {
@Mock private UserRepository userRepository;
@Mock private EmailService emailService;
@InjectMocks private UserService userService;
@Test @DisplayName("ユーザーIDでユーザーを取得できる") void testFindById_Success() { // Given Long userId = 1L; User user = createTestUser(userId, "Test User", "test@example.com"); when(userRepository.findById(userId)).thenReturn(Optional.of(user));
// When UserDTO result = userService.findById(userId);
// Then assertThat(result).isNotNull(); assertThat(result.getId()).isEqualTo(userId); assertThat(result.getName()).isEqualTo("Test User"); verify(userRepository).findById(userId); verifyNoInteractions(emailService); }
@Test @DisplayName("存在しないユーザーIDで例外がスローされる") void testFindById_NotFound() { // Given Long userId = 999L; when(userRepository.findById(userId)).thenReturn(Optional.empty());
// When & Then assertThatThrownBy(() -> userService.findById(userId)) .isInstanceOf(ResourceNotFoundException.class) .hasMessageContaining("User") .hasMessageContaining("999"); }
@Test @DisplayName("新しいユーザーを作成できる") void testCreateUser_Success() { // Given UserCreateRequest request = new UserCreateRequest(); request.setName("New User"); request.setEmail("new@example.com"); request.setPassword("password123");
User savedUser = createTestUser(1L, "New User", "new@example.com"); when(userRepository.existsByEmail("new@example.com")).thenReturn(false); when(userRepository.save(any(User.class))).thenReturn(savedUser); doNothing().when(emailService).sendWelcomeEmail(anyString());
// When UserDTO result = userService.create(request);
// Then assertThat(result).isNotNull(); assertThat(result.getId()).isEqualTo(1L); assertThat(result.getName()).isEqualTo("New User"); verify(userRepository).existsByEmail("new@example.com"); verify(userRepository).save(any(User.class)); verify(emailService).sendWelcomeEmail("new@example.com"); }
@Test @DisplayName("重複したメールアドレスでユーザー作成時に例外がスローされる") void testCreateUser_DuplicateEmail() { // Given UserCreateRequest request = new UserCreateRequest(); request.setName("New User"); request.setEmail("existing@example.com"); request.setPassword("password123");
when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);
// When & Then assertThatThrownBy(() -> userService.create(request)) .isInstanceOf(DuplicateResourceException.class) .hasMessageContaining("User") .hasMessageContaining("existing@example.com");
verify(userRepository, never()).save(any(User.class)); verify(emailService, never()).sendWelcomeEmail(anyString()); }
private User createTestUser(Long id, String name, String email) { User user = new User(); user.setId(id); user.setName(name); user.setEmail(email); return user; }}データベース統合テスト
Section titled “データベース統合テスト”@SpringBootTest@Transactional@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)class UserServiceIntegrationTest {
@Autowired private UserService userService;
@Autowired private UserRepository userRepository;
@Test @DisplayName("ユーザーの作成と取得の統合テスト") void testCreateAndFindUser() { // Given UserCreateRequest request = new UserCreateRequest(); request.setName("Integration Test User"); request.setEmail("integration@example.com"); request.setPassword("password123");
// When UserDTO createdUser = userService.create(request);
// Then assertThat(createdUser).isNotNull(); assertThat(createdUser.getId()).isNotNull(); assertThat(createdUser.getName()).isEqualTo("Integration Test User"); assertThat(createdUser.getEmail()).isEqualTo("integration@example.com");
// データベースから取得して確認 Optional<User> savedUser = userRepository.findById(createdUser.getId()); assertThat(savedUser).isPresent(); assertThat(savedUser.get().getName()).isEqualTo("Integration Test User"); }
@Test @DisplayName("トランザクションのロールバックテスト") void testTransactionRollback() { // Given UserCreateRequest request = new UserCreateRequest(); request.setName("Rollback Test User"); request.setEmail("rollback@example.com"); request.setPassword("password123");
// When try { userService.createWithError(request); } catch (Exception e) { // エラーが発生 }
// Then: トランザクションがロールバックされているため、データは保存されていない Optional<User> user = userRepository.findByEmail("rollback@example.com"); assertThat(user).isEmpty(); }}Web層のテスト
Section titled “Web層のテスト”MockMvcを使用したテスト
Section titled “MockMvcを使用したテスト”@WebMvcTest(UserController.class)class UserControllerTest {
@Autowired private MockMvc mockMvc;
@MockBean private UserService userService;
@Autowired private ObjectMapper objectMapper;
@Test @DisplayName("ユーザー一覧を取得できる") void testGetUsers() throws Exception { // Given List<UserDTO> users = Arrays.asList( createUserDTO(1L, "User 1", "user1@example.com"), createUserDTO(2L, "User 2", "user2@example.com") ); when(userService.findAll()).thenReturn(users);
// When & Then mockMvc.perform(get("/api/users")) .andExpect(status().isOk()) .andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$[0].id").value(1L)) .andExpect(jsonPath("$[0].name").value("User 1")) .andExpect(jsonPath("$[1].id").value(2L)) .andExpect(jsonPath("$[1].name").value("User 2")); }
@Test @DisplayName("ユーザー詳細を取得できる") void testGetUser() throws Exception { // Given Long userId = 1L; UserDTO userDTO = createUserDTO(userId, "Test User", "test@example.com"); when(userService.findById(userId)).thenReturn(userDTO);
// When & Then mockMvc.perform(get("/api/users/{id}", userId)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(userId)) .andExpect(jsonPath("$.name").value("Test User")) .andExpect(jsonPath("$.email").value("test@example.com")); }
@Test @DisplayName("存在しないユーザーIDで404エラーが返される") void testGetUser_NotFound() throws Exception { // Given Long userId = 999L; when(userService.findById(userId)) .thenThrow(new ResourceNotFoundException("User", userId));
// When & Then mockMvc.perform(get("/api/users/{id}", userId)) .andExpect(status().isNotFound()); }
@Test @DisplayName("新しいユーザーを作成できる") void testCreateUser() throws Exception { // Given UserCreateRequest request = new UserCreateRequest(); request.setName("New User"); request.setEmail("new@example.com"); request.setPassword("password123");
UserDTO createdUser = createUserDTO(1L, "New User", "new@example.com"); when(userService.create(any(UserCreateRequest.class))).thenReturn(createdUser);
// When & Then mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value(1L)) .andExpect(jsonPath("$.name").value("New User")) .andExpect(jsonPath("$.email").value("new@example.com")); }
@Test @DisplayName("バリデーションエラーで400エラーが返される") void testCreateUser_ValidationError() throws Exception { // Given UserCreateRequest request = new UserCreateRequest(); // 必須フィールドを設定しない
// When & Then mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()); }
private UserDTO createUserDTO(Long id, String name, String email) { UserDTO dto = new UserDTO(); dto.setId(id); dto.setName(name); dto.setEmail(email); return dto; }}リポジトリ層のテスト
Section titled “リポジトリ層のテスト”@DataJpaTest@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)class UserRepositoryTest {
@Autowired private UserRepository userRepository;
@Autowired private TestEntityManager entityManager;
@Test @DisplayName("メールアドレスでユーザーを検索できる") void testFindByEmail() { // Given User user = new User(); user.setName("Test User"); user.setEmail("test@example.com"); user.setPassword("password"); entityManager.persistAndFlush(user);
// When Optional<User> found = userRepository.findByEmail("test@example.com");
// Then assertThat(found).isPresent(); assertThat(found.get().getEmail()).isEqualTo("test@example.com"); assertThat(found.get().getName()).isEqualTo("Test User"); }
@Test @DisplayName("存在しないメールアドレスで空のOptionalが返される") void testFindByEmail_NotFound() { // When Optional<User> found = userRepository.findByEmail("notfound@example.com");
// Then assertThat(found).isEmpty(); }
@Test @DisplayName("メールアドレスの存在確認ができる") void testExistsByEmail() { // Given User user = new User(); user.setName("Test User"); user.setEmail("test@example.com"); user.setPassword("password"); entityManager.persistAndFlush(user);
// When boolean exists = userRepository.existsByEmail("test@example.com"); boolean notExists = userRepository.existsByEmail("notfound@example.com");
// Then assertThat(exists).isTrue(); assertThat(notExists).isFalse(); }}テストのベストプラクティス
Section titled “テストのベストプラクティス”1. テストの独立性
Section titled “1. テストの独立性”各テストは独立して実行できるようにします。
@SpringBootTest@Transactionalclass IndependentTest { // 各テストメソッドは独立して実行される // データベースの状態は各テスト後にロールバックされる}2. テストデータの準備
Section titled “2. テストデータの準備”@Testvoid testWithTestData() { // @BeforeEachでテストデータを準備 // または、@Sqlアノテーションを使用}3. アサーションの明確化
Section titled “3. アサーションの明確化”@Testvoid testWithClearAssertions() { // Given // When UserDTO result = userService.findById(1L);
// Then assertThat(result) .isNotNull() .extracting(UserDTO::getId, UserDTO::getName) .containsExactly(1L, "Test User");}Spring Bootでのテストは、以下の層に分けて実装します:
- ユニットテスト: 単一のクラスやメソッドをテスト
- 統合テスト: 複数のコンポーネントを組み合わせてテスト
- Web層テスト: コントローラーのエンドポイントをテスト
- リポジトリ層テスト: データベースアクセスをテスト
適切なテスト戦略により、コードの品質と信頼性を向上させることができます。