| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 컨테이너
- 스프링 부트
- Spring Data JPA
- Web
- DLQ
- Routing Key
- dockerhub
- 페이징
- securitycontextholderfilter
- 쿠버네티스
- redis
- kafka
- Spring Container
- JPQL
- docker compose
- docker
- JPA
- Spring
- 서블릿 컨테이너
- AWS
- CORS
- JdbcTemplate
- DI
- JWT
- Dead Letter Queue
- @ComponentScan
- 지연 로딩
- mybatis
- @Transactional
- MSA
- Today
- Total
look-forest
Web MVC / Security / JPA 관련 test 본문
실제 빈을 사용하여 테스트할 경우
@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 |