Spring/문법

테스트 코드 개념 2 + Mock 을 이용한 테스트 코드 작성 예제

가지코딩 2025. 6. 11. 10:41

✅ 목차

  1. 테스트를 위한 Annotation
  2. 단위 테스트 작성 원칙
  3. Mocking 개념 및 사용 이유
  4. 행위 검증 vs 상태 검증
  5. [예제] 단위 테스트 - Repository
  6. [예제] 단위 테스트 - Service
  7. [예제] 단위 테스트 - Controller
  8. [예제] 유틸 테스트
  9. [예제] 통합 테스트

1. 테스트를 위한 Annotation

 

  • @DataJpaTest
    • JPA 관련 컴포넌트만 로드하여 Repository 레이어 단위 테스트용
    • 기본적으로 인메모리 DB(H2 등) 사용
  • @ExtendWith
    • JUnit5 확장 기능 적용용
    • 주로 MockitoExtension과 함께 Service, 클래스 단위 테스트에 사용
  • @WebMvcTest
    • Web Layer (Controller, Filter 등) 테스트용
    • @Controller, @ControllerAdvice 등 웹 관련 Bean만 로드
  • @SpringBootTest
    • 스프링 부트 전체 컨텍스트 로드, 통합 테스트용
    • 실제 서버 실행과 유사한 환경 제공

 


2. 단위 테스트 작성 원칙

  • 의존성은 Mock 객체로 분리하여 독립적으로 테스트
  • 테스트는 의존성의 하위부터 진행 (Repository → Service → Controller 순)
  • 데이터 준비(Given)가 복잡할 경우 FixtureMonkey 등 도구로 간소화
  • 단위 테스트는 의존 대상 클래스가 정상 동작함을 전제로 한다

3. Mocking 개념 및 사용 이유

Mocking: 테스트 대상이 의존하는 객체를 가짜(Mock)로 만들어 실제 객체 의존성 제거

 

  • 이유
    1. 외부 의존성 제거
    2. 테스트 범위 집중
    3. 예외 상황 강제 발생 가능
  • 주의사항
    • 과도한 Mock 사용은 실제 서비스 로직과 괴리 발생
    • 인터페이스 변경 시 Mock 설정 유지보수 부담
    • Mock 코드 증가로 테스트 가독성 저하

4. 행위 검증 vs 상태 검증

 

  • 행위 검증 (Behavior Validation)
    • 특정 메소드 호출 여부, 호출 횟수 등 행위 검증
    • Mockito verify() 사용
  • 상태 검증 (State Validation)
    • 기능 수행 후 결과값이나 DB 상태가 기대와 일치하는지 검증
    • 반환값, 저장 데이터 확인

 


5. [예제] 단위 테스트 - Repository

 

  • 목적: JPA Repository의 기능 및 쿼리 동작 검증
  • 특징: 실제 DB 대신 인메모리 DB를 사용해 빠르게 테스트
  • 사용 어노테이션: @DataJpaTest
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

}
@DataJpaTest
public class UserRepositoryTest {

    @Autowired
    UserRepository userRepository;

    @Test
    void 유저_저장_로직_테스트() {
        // given
        User user = new User("test", "email", "password");

        // when
        User savedUser = userRepository.save(user);

        // then
        Assertions.assertThat(savedUser.getName()).isEqualTo("test");
        Assertions.assertThat(savedUser.getEmail()).isEqualTo("email");
        Assertions.assertThat(savedUser.getPassword()).isEqualTo("password");
    }
}

 


6. [예제] 단위 테스트 - Service

 

  • 목적: 비즈니스 로직 및 의존 객체(Mock)와의 상호작용 검증
  • 특징: 외부 의존 객체(UserRepository)는 Mock으로 대체
  • 사용 어노테이션: @ExtendWith(MockitoExtension.class), @Mock, @InjectMocks
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    @Transactional(rollbackFor = Exception.class)
    public UserResponseDto saveUser(UserCreationDto userCreationDto) {

        String encodedPassword = PasswordEncoder.encode(userCreationDto.getPassword());

        User user = new User(
                userCreationDto.getName(),
                userCreationDto.getEmail(),
                encodedPassword
        );

        User savedUser = userRepository.save(user);

        return new UserResponseDto(savedUser.getId(), savedUser.getName(), savedUser.getEmail());
    }
}
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @InjectMocks
    UserService userService;

    @Mock
    UserRepository userRepository;

    @Test
    void 유저_저장_서비스_단위_테스트() {
        // given
        UserCreationDto userCreationDto = new UserCreationDto("name", "email", "password");
        User user = new User("name", "email", "password");

        when(userRepository.save(any(User.class))).thenReturn(user);

        // when
        UserResponseDto result = userService.saveUser(userCreationDto);

        // then
        assertEquals("name", result.getName());
        assertEquals("email", result.getEmail());

        verify(userRepository, times(1)).save(any(User.class));
    }
}

 


7. [예제] 단위 테스트 - Controller

 

  • 목적: 웹 계층(HTTP 요청/응답) 단위 테스트
  • 특징: 내부 서비스는 Mock(@MockBean)으로 대체, MockMvc 사용
  • 사용 어노테이션: @WebMvcTest, @MockBean
@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/users")
    public UserResponseDto create(@Valid @RequestBody UserCreationDto creationDto) {
        return userService.saveUser(creationDto);
    }
}

 

@WebMvcTest(UserController.class)
public class UserControllerTest {

    @MockBean
    UserService userService;

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    void 유저_저장_컨트롤러_테스트() throws Exception {
        // given
        UserCreationDto userCreationDto = new UserCreationDto("name", "email", "password");
        UserResponseDto userResponseDto = new UserResponseDto(1L, "name", "email");

        when(userService.saveUser(any(UserCreationDto.class))).thenReturn(userResponseDto);

        // when
        ResultActions result = mockMvc.perform(
            post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userCreationDto))
        );

        // then
        result.andExpect(status().isOk())
              .andExpect(jsonPath("$.name").value("name"))
              .andExpect(jsonPath("$.email").value("email"));
    }
}

8. [예제] 유틸 테스트

 

  • 목적: 단순 로직이나 기능을 검증하는 순수 메서드 테스트
  • 특징: Mock 없이 동작, 순수 자바 메서드 테스트에 적합

 

public class PasswordEncoder {

    public static String encode(String rawPassword) {
        return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
    }

    public static boolean matches(String rawPassword, String encodedPassword) {
        BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
        return result.verified;
    }
}

 

 

@ExtendWith(MockitoExtension.class)
public class PasswordEncoderTest {

    @Test
    void 비밀번호_검사_성공() {
        //given
        String rawPassword = "password";
        String encodedPassword = PasswordEncoder.encode(rawPassword);

        //when
        boolean isMatched = PasswordEncoder.matches(rawPassword, encodedPassword);

        //then
        assertTrue(isMatched);
    }

    @Test
    void 비밀번호_검사_실패() {
        //given
        String rawPassword = "password";
        String encodedPassword = PasswordEncoder.encode("password2");

        //when
        boolean isMatched = PasswordEncoder.matches(rawPassword, encodedPassword);

        //then
        assertFalse(isMatched);
    }
}

9. [예제] 통합 테스트

 

  • 목적: 애플리케이션 전체 동작 흐름 검증 (웹 + DB)
  • 특징: 실제 컨텍스트를 띄우고 Mock 없이 동작
  • 사용 어노테이션: @SpringBootTest, @AutoConfigureMockMvc
@SpringBootTest
@AutoConfigureMockMvc
public class UserIntegrationTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    void 유저_저장_통합_테스트() throws Exception {
        // given
        UserCreationDto userCreationDto = new UserCreationDto("name", "email", "password");

        // when
        ResultActions result = mockMvc.perform(
            post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userCreationDto))
        );

        // then
        String contentAsString = result.andReturn().getResponse().getContentAsString();
        UserResponseDto actualResult = objectMapper.readValue(contentAsString, UserResponseDto.class);

        result.andExpect(status().isOk());
        assertThat(actualResult.getId()).isGreaterThan(0L);
        assertEquals("email", actualResult.getEmail());
        assertEquals("name", actualResult.getName());
    }
}

 

 

 


전체 코드

https://github.com/gajicoding/spring-test-code

 

GitHub - gajicoding/spring-test-code

Contribute to gajicoding/spring-test-code development by creating an account on GitHub.

github.com