✅ 목차
- 테스트를 위한 Annotation
- 단위 테스트 작성 원칙
- Mocking 개념 및 사용 이유
- 행위 검증 vs 상태 검증
- [예제] 단위 테스트 - Repository
- [예제] 단위 테스트 - Service
- [예제] 단위 테스트 - Controller
- [예제] 유틸 테스트
- [예제] 통합 테스트
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)로 만들어 실제 객체 의존성 제거
- 이유
- 외부 의존성 제거
- 테스트 범위 집중
- 예외 상황 강제 발생 가능
- 주의사항
- 과도한 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
'Spring > 문법' 카테고리의 다른 글
@EntityGraph - fetch join을 어노테이션으로 처리하기 (0) | 2025.06.11 |
---|---|
테스트 커버리지 - JaCoCo 설정 (3) | 2025.06.11 |
테스트 코드 개념 (0) | 2025.06.11 |
JSON 포맷 변경하기: Jackson 설정 커스터마이징 (1) | 2025.05.23 |
Spring Boot Logger 사용법 – SLF4J & Logback (0) | 2025.05.23 |