Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

Web MVC / Security / JPA 관련 test 본문

Test/애플리케이션을 테스트하는 다양한 방법

Web MVC / Security / JPA 관련 test

studyHub 2024. 12. 15. 21:07

실제 빈을 사용하여 테스트할 경우

@SpringBootTest : 통합 테스트를 위해 SpringBoot 애플리케이션 컨텍스트 전체를 로드

  • webEnvironment 속성 : 테스트 환경에서 애플리케이션의 웹 계층을 어떻게 구성할지 결정
    • MOCK (기본값) : 내장 Tomcat을 실행하지 않고, Mock 서블릿 환경에서 테스트를 수행
    • RANDOM_PORT : 애플리케이션을 실제로 실행하고, 랜덤 포트에서 테스트 수행 (주로 통합 테스트에 사용)
    • DEFINED_PORT : 애플리케이션을 실제로 실행하고, application.properties에 정의된 포트에서 실행
    • NONE : 웹 계층 없이 테스트 수행

@AutoConfigureMockMvc : MockMvc를 자동으로 구성

  • MockMvc : 요청과 응답을 시뮬레이션하기 위한 도구로, 실제 HTTP 호출 없이 컨트롤러를 테스트
@AutoConfigureMockMvc //MockMvc를 자동으로 구성
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) // Tomcat을 실행하지 않고, Mock 서블릿 환경에서 테스트를 수행
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void 회원가입_정상() throws Exception {
        //given
        JoinReqDto reqDto = new JoinReqDto();
        reqDto.setUsername("cjl0701");
        reqDto.setPassword("123456");
        reqDto.setEmail("cjl0701@gmail.com");
        reqDto.setFullname("최재량");

        String requestBody = objectMapper.writeValueAsString(reqDto);

        //when & then
        mockMvc.perform(post("/api/join")
                        .content(requestBody)
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isCreated())
                .andExpect(jsonPath("data.id").exists())
                .andExpect(jsonPath("data.username").value("cjl0701"))
        ;
    }
}

 

 

컨트롤러만 테스트할 경우

@WebMvcTest : 컨트롤러 계층만 테스트. 서비스 및 리포지토리는 Mocking 필요

@WebMvcTest는 컨트롤러와 관련된 웹 계층의 컴포넌트만 로드하는 "슬라이스 테스트(slice test)"를 제공.
따라서 컨트롤러가 의존하고 있는 서비스나 리포지토리 같은 빈들은 자동으로 로드되지 않음.
이때, @MockitoBean을 사용하면, 모의(mock) 빈을 컨텍스트에 등록해서, 컨트롤러의 의존성을 해결할 수 있다.

 

@WebMvcTest(SampleController.class)
public class SampleControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private MyService myService; // 서비스 계층은 Mock으로 주입

    @Test
    public void testGetUsers() throws Exception {
        when(myService.getUsers()).thenReturn(List.of(new User(1, "Alice")));

        mockMvc.perform(get("/api/users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].name").value("Alice"));
    }
}

 

 


Security 인증이 필요한 경우

@WithUserDetails Spring Security가 사용하는 UserDetailsService에서 사용자 정보를 로드한다.

※ 이때 영속성 컨텍스트에 유저 엔티티 정보가 남는다. 정확한 쿼리를 보기 위해선 em.clear() 필요

@BeforeEach
public void setUp() {
    userRepository.save(newUser("cjl0701", "최재량"));
}

//setupBefore=TEST_METHOD : setUp 메서드 실행 전에 수행됨
//setupBefore = TestExecutionEvent.TEST_EXECUTION : 테스트 메서드 실행 전에 수행됨
@WithUserDetails(value = "cjl0701", setupBefore = TestExecutionEvent.TEST_EXECUTION) //DB에서 username으로 조회해서 세션에 담아준다.
@Test
public void saveAccount_test() throws Exception {
    //given
    AccountSaveReqDto accountSaveReqDto = new AccountSaveReqDto();
    accountSaveReqDto.setNumber(9999L);
    accountSaveReqDto.setPassword(1234L);
    String requestBody = objectMapper.writeValueAsString(accountSaveReqDto);

    //when & then
    mockMvc.perform(post("/api/s/account")
                    .content(requestBody)
                    .contentType(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.data.number").value(9999L));
}

 

그러기 위해선 JWT 인증 필터 구현 시, 토큰이 없을 경우 다음 필터로 넘어가 SecurityContextHolder를 뒤지도록 짜두어야 한다.

/**
 * jwt 토큰이 있을 경우 검증을 진행하지만, 토큰이 없을 경우 다음 필터로 넘어간다.
 * 따라서 테스트 시에는 인가 필터에서 인증된 상태로 세션만 만들어주면 된다.
 */
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    if (isHeaderVerify(request)) {
        String token = request.getHeader(JwtVO.HEADER_STRING).replace(JwtVO.TOKEN_PREFIX, "");
        LoginUser loginUser = JwtProcess.verify(token);

        Authentication authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }
    chain.doFilter(request, response);
}

 

테스트 시 가짜 사용자 정보를 간단히 설정 (Spring Security와 연동된 실제 사용자 데이터 필요 없음)할 경우는, 

@WithMockUser 를 사용하면 된다.

 

※ 실제 쿼리 확인하기

//User 정보 조회할 때 영속성컨텍스트에 User 정보가 저장된다. 정확한 쿼리를 보기 위해선 em.clear로 초기화 해주자.
@WithUserDetails(value = "cjl0701", setupBefore = TestExecutionEvent.TEST_EXECUTION)
@Test
public void 계좌삭제_test() throws Exception {
    em.clear();
    //given
    Long number = 1111L;

    //when & then
    mockMvc.perform(delete("/api/s/account/" + number))
            .andDo(print())
            .andExpect(status().isOk());

    // JUnit 테스트에서 delete 쿼리는 가장 마지막에 오면 발동 안됨.
    assertThrows(CustomApiException.class, () -> accountRepository.findByNumber(number)
            .orElseThrow(() -> new CustomApiException("계좌를 찾을 수 없습니다.")));
}

 

🔥 @WithMockUser vs @WithUserDetails 비교

@WithMockUser 가짜 사용자(Mock User) DB 조회 X 빠른 테스트 가능, 간단한 테스트에 적합 사용자 정보를 세부적으로 설정 불가
@WithUserDetails 실제 UserDetails DB에서 조회 실제 DB 데이터 기반 테스트 가능 속도가 다소 느릴 수 있음, DB가 필요

 


테이블 PK 초기화를 위해 teardown.sql 적용하기

@Transcation을 걸어도, @BeforeEach로 초기화 데이터를 넣어줄때 자동 생성되는 PK 값은 초기화가 되지 않아 문제가 발생하곤 한다. (당연히 PK가 1인줄 알고 테스트 코드를 짰는데 2인 경우가 발생)

따라서 @BeforeEach 마다 테이블을 초기화하기 위한 teardown.sql을 적용하자.

 

1. resource/db 디렉토리에 아래 sql을 적용

SET REFERENTIAL_INTEGRITY FALSE; --제약조건 비활성화
truncate table transaction_tb;
truncate table account_tb;
truncate table user_tb;
SET REFERENTIAL_INTEGRITY TRUE;

 

2. 테스트 클래스에 @Sql로 import하면 @BeforeEach 실행마다 해당 sql이 실행된다.

@ActiveProfiles("test") //dev 모드에서 초기화 데이터를 넣으므로 분리
//@Transactional
@Sql("classpath:db/teardown.sql") //PK 초기화를 위해 @BeforeEach 실행마다 테이블 초기화
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class AccountControllerTest extends DummyObject {
    @Autowired
    private EntityManager em;

    @BeforeEach
    public void setUp() {
        User cjl0701 = userRepository.save(newUser("cjl0701", "최재량"));
        accountRepository.save(newAccount(1111L, cjl0701));
        em.clear(); //쿼리 확인을 위해 영속성 컨텍스트 비우기
    }
    
    @WithUserDetails(value = "cjl0701", setupBefore = TestExecutionEvent.TEST_EXECUTION) //DB에서 username으로 조회해서 세션에 담아준다.
    @Test
    public void saveAccount_test() throws Exception {
        //given
        Long userId = 1L; // <- 자동 생성된 PK 값이 2L일수도 있다..!
        ...
}

@DataJpaTest

@DataJpaTest의 기본 동작

  • JPA 관련 컴포넌트(예: @Repository)만 로드하며, 주로 테스트 환경에서 H2 같은 임베디드 데이터베이스를 사용
  • 롤백(@Transactional)을 기본적으로 수행
  • JPA 관련 테스트에 최적화되어 있지만, 데이터 초기화 작업이 중요하다면 @SpringBootTest를 사용하는 것이 더 적합할 수 있다.

※ PK 초기화 스크립트 관련

@DataJpaTest는 데이터베이스 연결 및 초기화를 위한 스크립트(schema.sql 또는 data.sql 등)를 자동으로 실행하는데, 이 과정에서 외부 SQL 파일(teardown.sql)을 명시적으로 실행해야 할 때 충돌이 발생할 수 있다. @Sql 적용 순서 문제나, H2는 기본적으로 테이블을 생성하거나 초기화할 때 제약 조건을 다시 활성화하기 때문이다.

 

 

'Test > 애플리케이션을 테스트하는 다양한 방법' 카테고리의 다른 글

Mockito  (0) 2024.11.10
Junit 5  (2) 2024.11.10
테스트 - DB 연동  (0) 2024.08.17
테스트의 종류  (0) 2021.05.01