프레임워크/스프링

[스프링] 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호