프레임워크/스프링
[스프링] Controller 단위테스트 VS 통합테스트
believekim
2025. 6. 26. 17:39
가능한 부분은 최대한 단위테스트를 하는 것이 좋다.
테스트 실행 속도가 빠르고 외부 영향없이 해당 클래스에 집중할 수 있기 떄문이다.
시스템 환경
IDE | IntelliJ IDEA |
Language | Java 17 |
Framework | Spring Boot 3.4.5 |
Build Tool | Gradle |
ORM | JPA (Hibernate) |
Database | MySQL |
Security | Spring Security + JWT (jjwt 0.11.5) |
1. 테스트 목적과 범위
구분 | 단위 테스트 | 통합 테스트 |
테스트 목적 | Controller 단위 동작 검증, 외부 서비스(mock) 의존 |
Controller부터 Repository, DB까지 실제 동작 검증 |
주요 검증 포인트 | HTTP 요청, 응답 상태, JSON 응답 바디 등 컨트롤러 레벨 검증 |
전체 비즈니스 로직 및 DB 연동 포함, 실제 인증 및 권한 동작 검증 |
외부 의존성 영향도 | 없음 (서비스 계층을 mock 처리) | 실제 서비스, DB와 상호작용 발생 |
2. 테스트 환경 및 설정
구분 | 단위 테스트 | 통합 테스트 |
테스트 어노테이션 | @WebMvcTest(UserController.class) 특정 컨트롤러만 테스트, 슬라이스 테스트 |
@SpringBootTest 애플리케이션 전체 컨텍스트 로딩 |
MockMvc 설정 | @AutoConfigureMockMvc(addFilters = false) 필터 비활성화 (보안 관련 제외) | @AutoConfigureMockMvc 실제 보안 필터 등 전체 컨텍스트 적용 |
외부 의존성 주입 | @MockitoBean로 서비스 레이어를 목(mock) 객체로 주입 |
실제 Bean 주입 (UserRepository, PasswordEncoder 등 실제 구현 사용) |
보안 컨텍스트 | @WithMockUser 어노테이션으로 인증된 사용자 시뮬레이션 | SecurityContextHolder에 직접 인증 객체 설정 (실제 권한 부여) |
3. 통합테스트 구조 및 특징
- 실제 애플리케이션 컨텍스트 전체 로딩 (@SpringBootTest)
- UserRepository를 통해 테스트용 실제 사용자 엔티티 DB 저장
- 테스트 실행 전 @BeforeEach에서 SecurityContext에 직접 인증 정보 세팅(선택)
- 비밀번호 암호화 등 실제 비즈니스 로직 포함
- DB 트랜잭션 롤백(@Transactional)을 통한 테스트 환경 초기화
- 테스트 실행 시간이 상대적으로 더 걸림
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class AuthControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@Autowired private UserRepository userRepository;
@Autowired private PasswordEncoder passwordEncoder;
private User testUser;
@BeforeEach
void setUp() {
testUser = userRepository.save(
User.builder()
.email("user@email.com")
.password(passwordEncoder.encode("OldPassword12!@"))
.name("테스트유저")
.nickname("testnick")
.userRole(UserRole.USER)
.build()
);
}
@Test
void 로그인_성공() throws Exception {
LoginRequest request = new LoginRequest(
"user@email.com","OldPassword12!@"
);
mockMvc.perform(post("/api/auth/login")
.with(user("1").roles("USER"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk());
}
@Test
void 로그아웃_성공() throws Exception {
LoginRequest request = new LoginRequest(
"user@email.com","OldPassword12!@"
);
mockMvc.perform(post("/api/auth/logout")
.with(user("1").roles("USER"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk());
}
@Test
void 토큰_재발급_성공() throws Exception {
LoginRequest request = new LoginRequest("user@email.com", "OldPassword12!@");
String responseBody = mockMvc.perform(post("/api/auth/login")
.with(user("1").roles("USER"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
TokenResponse tokenResponse = objectMapper.readValue(responseBody, TokenResponse.class);
mockMvc.perform(post("/api/auth/reissue")
.header("Authorization", "Bearer " + tokenResponse.getRefreshToken()))
.andExpect(status().isOk());
}
}
4. 단위테스트 구조 및 특징
- UserService를 @MockitoBean으로 목 객체 주입
- 보안 필터(JWT) 비활성화 (addFilters = false)
- @WithMockUser를 통해 인증된 사용자 시나리오 처리
- 테스트 케이스별로 서비스 호출 동작을 when(), doNothing() 등으로 정의
- HTTP 요청 빌더(mockMvc.perform) 후 응답 상태 및 JSON 검증(jsonPath) 수행
- 테스트 실행 속도가 빠르고, 외부 영향 없이 컨트롤러 로직만 집중 검증
@WebMvcTest(UserController.class)
@AutoConfigureMockMvc(addFilters = false)
@Import(TestConfig.class)
public class UserControllerTest {
@MockitoBean
private UserService userService;
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void 사용자_생성_성공() throws Exception {
UserCreateRequest request = new UserCreateRequest(
"test3@email.com", "!Aa123456", "홍길동", "gildong", UserRole.USER
);
doNothing().when(userService).createUser(any(UserCreateRequest.class));
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
.andExpect(status().isCreated());
}
@Test
@WithMockUser
void 사용자_조회_성공() throws Exception {
UserResponse response = new UserResponse(1L, "test@email.com", "testname", "testnickname");
when(userService.findById(any())).thenReturn(response);
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("test@email.com"))
.andExpect(jsonPath("$.name").value("testname"))
.andExpect(jsonPath("$.nickname").value("testnickname"));
}
@Test
@WithMockUser
void 사용자_수정_성공() throws Exception {
UserUpdateRequest updateRequest = new UserUpdateRequest("홍길순", "Oldpassword12!@", "Newpassword12!@");
doNothing().when(userService).updateUser(any(UserUpdateRequest.class), any(UserAuth.class));
mockMvc.perform(patch("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateRequest))
)
.andExpect(status().isOk());
}
@Test
@WithMockUser
void 사용자_삭제_성공() throws Exception {
doNothing().when(userService).deleteUser(any(UserAuth.class));
mockMvc.perform(delete("/api/users"))
.andExpect(status().isOk());
}
}
5. 장단점
구분 | 단위 테스트 | 통합 테스트 |
장점 | - 실행 속도가 빠름 - 컨트롤러 로직에 집중 가능 - 외부 영향 없이 테스트 안정성 높음 - @WithMockUser로 인증 처리 간편 |
- 실제 DB 및 서비스 계층과의 연동 검증 가능 - 실제 보안 필터 적용 - 운영 환경과 유사한 테스트 가능 |
단점 | - 실제 DB 연동 검증 불가 - 외부 서비스와의 통합 시나리오 테스트 불가능 - mock 설정이 많아 복잡할 수 있음 - 보안 필터 비활성화로 실제 인증 흐름 테스트 불가 |
- 실행 속도가 느림 - 보안 컨텍스트 직접 설정 필요 - 설정 및 유지보수 복잡 - 외부 요인에 따라 테스트 불안정 가능 |
6. 속도 비교
- 단위 테스트 약 5초 / 통합테스트 약 24호