spring

테스트 코드

seungmin576 2025. 3. 6. 13:06
  • H2 (Repository 테스트 or 통합 테스트용)
dependencies {
    testRuntimeOnly 'com.h2database:h2'
}

 

 

Annotation 종류

1. @DataJpaTest

  • JPA와 관련된 Component들만 모두 가지고와 Repository Layer 단위 테스트를 위한 Annotation이다.
  • 기본적으로 In-memory DB(ex H2)를 사용하여 테스팅

요약: JPA 레포지토리 단위 테스트

2. @ExtendWith

  • Junit5 환경에서 확장기능을 사용할때 사용.
  • 주로 MockitoExtension 과 함께 Service Layer, 클래스 단위 테스트에 사용

요약: 서비스 단위 테스트

3. @WebMvcTest

  • 스프링의 Web Layer(controller, filter 등)을 테스트하기 위한 Annotation이다.
  • @Controller, @ControllerAdvice 등 웹과 관련된 Bean만 로드하여 테스트를 수행한다.

요약: 컨트롤러 단위 테스트

4. @SpringBootTest

  • 스프링 부트 전체를 테스트 수행하기 위해서 사용하는 Annotation이다.
  • 서버를 실행하듯 모든 스프링 Context를 로드한다.

요약: 통합 테스트

 

 

 

 

Mocking

 

테스트 코드를 작성하면 테스트를 하기 위한 코드 이외의 의존 객체들이 존재하는 경우가 있습니다. 예를 들면 Service 코드를 테스트 하는데 Repository가 필요한 경우이죠.

사용 이유

  1. 외부 의존성 제거
  2. 테스트 범위 준수
  3. 에러 상황 강제 발생

행위 검증(Behavior Validation), 상태 검증(State Validation)

  • 행위 검증
    • **“테스트시 특정한 행위를 하였는가”**를 확인하는 행위이다.
    • 테스트를 하려는 코드가 어떤 메소드를 호출하였는지 하지 않았는지, 몇번 수행했는지 확인.
    • Mockito의 verify() 메소드를 통해 검증
  • 상태 검증
    • **“기능 수행 후 결과값이 기대값과 일치하는가?”**를 확인하는 행위이다.
    • 반환값, DB 저장된 데이터 등 값에 대해 의도한대로 반환/저장이 되는지 확인.
  • application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;
spring.datasource.username=root
spring.datasource.driver-class-name=org.h2.Driver

spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database=h2

(default로 테스트 코드는 H2를 사용)

 


Repository Test

테스트 대상

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void 이메일로_사용자를_조회할_수_있다() {
        // given
        String email = "asd@asd.com";
        User user = new User(email, "password", UserRole.USER);
        userRepository.save(user);

        // when
        User foundUser = userRepository.findByEmail(email).orElse(null);

        // then
        assertNotNull(foundUser);
        assertEquals(email, foundUser.getEmail());
        assertEquals(UserRole.USER, foundUser.getUserRole());
    }
}

 

 

 

 

 

Service 테스트

  • @Mock: 테스트 대상 외의 의존 객체
  • @InjectMocks: 테스트 대상 객체

 

케이스1

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock // 테스트 대상 아님
    private UserRepository userRepository;

    @InjectMocks // 테스트 대상
    private UserService userService;

    @Test
    void User를_ID로_조회할_수_있다() {
        // given
        String email = "asd@asd.com";
        long userId = 1L;
        User user = new User(email, "password", UserRole.USER);
        ReflectionTestUtils.setField(user, "id", userId);

        given(userRepository.findById(anyLong())).willReturn(Optional.of(user));

        // when
        UserResponse userResponse = userService.getUser(userId);

        // then
        assertThat(userResponse).isNotNull();
        assertThat(userResponse.getId()).isEqualTo(userId);
        assertThat(userResponse.getEmail()).isEqualTo(email);
    }

    @Test
    void 존재하지_않는_User를_조회_시_InvalidRequestException을_던진다() {
        // Given
        long userId = 1L;
        given(userRepository.findById(anyLong())).willReturn(Optional.empty());

        // When & Then
        assertThrows(InvalidRequestException.class,
                () -> userService.getUser(userId),
                "User not found"
        );
    }

 

 

 

케이스2

    @Test
    void User를_삭제할_수_있다() {
        // given
        long userId = 1L;
        given(userRepository.existsById(anyLong())).willReturn(true);
        doNothing().when(userRepository).deleteById(anyLong());

        // when
        userService.deleteUser(userId);

        // then
        verify(userRepository, times(1)).deleteById(userId);
    }

    @Test
    void 존재하지_않는_User를_삭제_시_InvalidRequestException를_던진다() {
        // given
        long userId = 1L;
        given(userRepository.existsById(userId)).willReturn(false);

        // when & then
        assertThrows(InvalidRequestException.class, () -> userService.deleteUser(userId));
        verify(userRepository, never()).deleteById(userId);
    }
}

 

 

 

 

 

 

Controller Test

@MockBean: Bean을 Mocking(3.4.x 이전. 3.4.x부터는 deprecated되어 사용 불가)

@MockitoBean: Bean을 Mocking(3.4.x 이후)

MockMvc: 스프링 MVC 컨트롤러를 테스트할 수 있게 해주는 모의 HTTP 요청/응답 도구

MockMvcTester: MockMvc의 래핑(상위 추상화) 버전, AssertJ와 더 잘 통합될 수 있음**(3.4.x 이후)**

 

import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void User_목록_조회_빈리스트() throws Exception {
        // given
        given(userService.getUsers()).willReturn(List.of());

        // when & then
        mockMvc.perform(get("/users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isEmpty());
    }

    @Test
    void User_목록_조회() throws Exception {
        // given
        long userId1 = 1L;
        long userId2 = 2L;
        String email1 = "user1@a.com";
        String email2 = "user2@a.com";
        List<UserResponse> userList = List.of(
                new UserResponse(userId1, email1),
                new UserResponse(userId2, email2)
        );
        given(userService.getUsers()).willReturn(userList);

        // when & then
        mockMvc.perform(get("/users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(2))
                .andExpect(jsonPath("$[0].id").value(userId1))
                .andExpect(jsonPath("$[0].email").value(email1))
                .andExpect(jsonPath("$[1].id").value(userId2))
                .andExpect(jsonPath("$[1].email").value(email2));
    }

    @Test
    void User_단건_조회() throws Exception {
        // given
        long userId = 1L;
        String email = "a@a.com";

        given(userService.getUser(userId)).willReturn(new UserResponse(userId, email));

        // when & then
        mockMvc.perform(get("/users/{userId}", userId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(userId))
                .andExpect(jsonPath("$.email").value(email));
    }
}

 

 

 

 

 

 

통합 테스트

@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserIntegrationTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private JwtUtil jwtUtil;

    @Test
    @DisplayName("회원가입 후 User 단건 조회를 한다.")
    void getUserIntegrationTest() {
        // 회원가입
        String signupUrl = "http://localhost:" + port + "/auth/signup";
        SignupRequest signupRequest = new SignupRequest("user@example.com", "password", "USER");
        ResponseEntity<SignupResponse> signupResponse = restTemplate.postForEntity(signupUrl, signupRequest, SignupResponse.class);

        // 회원가입 검증
        assertThat(signupResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        SignupResponse signupBody = signupResponse.getBody();
        assertThat(signupBody).isNotNull();
        String token = signupBody.getBearerToken();
        assertThat(token).isNotBlank();

        // JWT 에서 userId 추출
        String rawToken = jwtUtil.substringToken(token);
        Claims claims = jwtUtil.extractClaims(rawToken);
        Long userIdFromToken = Long.valueOf(claims.getSubject());
        assertThat(userIdFromToken).isNotNull();

        // User 정보 단건 조회
        String url = "http://localhost:" + port + "/users/" + userIdFromToken;
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", token);
        HttpEntity<Void> requestEntity = new HttpEntity<>(headers);

        ResponseEntity<UserResponse> userResponse = restTemplate.exchange(
                url,
                HttpMethod.GET,
                requestEntity,
                UserResponse.class
        );

        // 검증
        assertThat(userResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        UserResponse responseBody = userResponse.getBody();
        assertThat(responseBody).isNotNull();
        assertThat(responseBody.getId()).isEqualTo(userIdFromToken);
        assertThat(responseBody.getEmail()).isEqualTo("user@example.com");
    }
}