development-logo
Spring 애플리케이션을 개발하다 보면, 테스트 코드에서 실제 빈(Bean)을 사용하지 않고 모의 객체(Mock 객체) 로 대체해 테스트를 단순화하거나 특정 로직만 검증하고 싶을 때가 있습니다. 이러한 Mock 객체를 자동 생성/주입 해주는 기능을 제공하는 것이 대표적으로 Mockito 라이브러리입니다.
이번 글에서는 제가 수강중인 “Practical Testing: 실용적인 테스트 가이드” 강의를 통해 학습한 @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks에 대한 내용을 정리하고, 추가로 함께 진행중인 “인프런 워밍업 클럽 스터디 3기” 미션까지 공유해 보겠습니다.
아래 애너테이션들은 주로 org.mockito 패키지와 org.springframework.boot.test.mock.mockito 패키지에서 제공됩니다.
테스트 요구사항과 상황에 따라 위 애너테이션들을 적절히 조합합니다.
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
class MockTest {
@Mock
private UserRepository userRepository; // 가짜(Mock) 객체
@Test
void testFindUserWithMock() {
// @Mock 애너테이션이 동작하도록 초기화
MockitoAnnotations.openMocks(this);
// 가짜 객체이므로, 직접 동작을 세팅해야 함
when(userRepository.findUsernameById(1L)).thenReturn("MockUser");
// 실제 호출
String username = userRepository.findUsernameById(1L);
// 검증
assertEquals("MockUser", username);
}
}
Javaimport org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
class MockBeanTest {
@MockBean
private UserRepository userRepository; // Spring Context 내 실제 Bean을 이 Mock으로 교체
@Test
void testFindUserWithMockBean() {
// MockBean은 Spring Boot Test에서 자동으로 초기화되므로 openMocks 불필요
when(userRepository.findUsernameById(1L)).thenReturn("MockBeanUser");
String username = userRepository.findUsernameById(1L);
assertEquals("MockBeanUser", username);
}
}
Javaimport org.junit.jupiter.api.Test;
import org.mockito.Spy;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
class SpyTest {
static class SampleService {
public String sayHello(String name) {
return "Hello " + name;
}
}
@Spy
private SampleService sampleService;
@Test
void testSpy() {
MockitoAnnotations.openMocks(this);
// 원래는 실제 로직을 호출하지만, 특정 인자일 때는 가짜 반환값을 설정
when(sampleService.sayHello("Spy")).thenReturn("Hello Spy (stubbed)");
// 1) "Spy" 인자로 부르는 경우는 Stub값 사용
assertEquals("Hello Spy (stubbed)", sampleService.sayHello("Spy"));
// 2) 그 외 인자는 실제 메소드 호출
assertEquals("Hello John", sampleService.sayHello("John"));
}
}
Javaimport org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
class SpyBeanTest {
@SpyBean
private SampleService sampleService; // Spring Bean을 Spy로 치환
@Test
void testSpyBean() {
// Spring Boot Test에서 자동 초기화됨
when(sampleService.sayHello("SpyBean")).thenReturn("Hello SpyBean (stubbed)");
// 1) "SpyBean" 인자로 부르는 경우는 Stub값
assertEquals("Hello SpyBean (stubbed)", sampleService.sayHello("SpyBean"));
// 2) 그 외 인자는 실제 서비스 로직 호출
assertEquals("Hello John", sampleService.sayHello("John"));
}
}
Javaimport org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
class InjectMocksTest {
static class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String getUsername(Long id) {
return userRepository.findUsernameById(id);
}
}
interface UserRepository {
String findUsernameById(Long id);
}
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService; // userRepository 주입 대상
@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this);
}
@Test
void testInjectMocks() {
when(userRepository.findUsernameById(1L)).thenReturn("InjectMockUser");
String username = userService.getUsername(1L);
assertEquals("InjectMockUser", username);
}
}
Java위 본문에서 설명하였으니, 간략히 한 표로 정리해봅니다.
애노테이션 | 주로 사용 환경 | 스프링 빈 등록 여부 | 특징 |
---|---|---|---|
@Mock | Mockito (단위 테스트) | X (스프링 컨텍스트와 무관) | 순수 Mock 객체. 내부 로직 없이 호출 기록 및 Stubbing에 특화 |
@MockBean | Spring Boot Test | O (스프링 빈을 Mock 객체로 교체) | 실제 스프링 빈을 Mock으로 대체. 슬라이스/통합테스트 등에서 활용 |
@Spy | Mockito (단위 테스트) | X (스프링 컨텍스트와 무관) | Spy 객체. 기본적으로 실제 메서드 동작 + 필요한 부분만 가짜(Stubbing) |
@SpyBean | Spring Boot Test | O (스프링 빈을 Spy 객체로 교체) | 실제 스프링 빈을 Spy로 대체. 실제 로직이 동작하되, 부분적 모의 가능 |
@InjectMocks | Mockito (단위 테스트) | – | @Mock/@Spy 객체를 대상 클래스 내부 필드에 자동으로 주입 |
@BeforeEach를 사용하지 않고, 각 테스트 메서드 내부에서 필요한 데이터를 독립적으로 생성하는 구조로 구성했습니다. 이는 테스트마다 서로 다른 시나리오를 유연하게 구성하기 위해, 공통 데이터 생성을 한곳에 몰아넣지 않는 방식입니다.
@BeforeEach
void init() {
// 공통 데이터를 미리 생성하지 않습니다.
/*
* 각 테스트가 각각의 상황(사용자, 게시물, 댓글)을 자유롭게 구성할 수 있도록,
* 여기서는 사전에 아무것도 세팅해두지 않습니다.
* 테스트마다 필요한 데이터 타입이나 조건이 다를 수 있으므로,
* 각 테스트 메서드 내부에서 직접 객체를 생성하고 준비해 주는 방식을 채택했습니다.
*/
}
@DisplayName("사용자가 댓글을 작성할 수 있다.")
@Test
void writeComment() {
// given
// 1-1. 사용자 생성에 필요한 내용 준비
// 1-2. 사용자 생성
// 1-3. 게시물 생성에 필요한 내용 준비
// 1-4. 게시물 생성
// 1-5. 댓글 생성에 필요한 내용 준비
// when
// 1-6. 댓글 생성
// then
// 검증
}
@DisplayName("사용자가 댓글을 수정할 수 있다.")
@Test
void updateComment() {
// given
// 2-1. 사용자 생성에 필요한 내용 준비
// 2-2. 사용자 생성
// 2-3. 게시물 생성에 필요한 내용 준비
// 2-4. 게시물 생성
// 2-5. 댓글 생성에 필요한 내용 준비
// 2-6. 댓글 생성
// when
// 2-7. 댓글 수정
// then
// 검증
}
@DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.")
@Test
void cannotUpdateCommentWhenUserIsNotWriter() {
// given
// 3-1. 사용자1 생성에 필요한 내용 준비
// 3-2. 사용자1 생성
// 3-3. 사용자2 생성에 필요한 내용 준비
// 3-4. 사용자2 생성
// 3-5. 사용자1의 게시물 생성에 필요한 내용 준비
// 3-6. 사용자1의 게시물 생성
// 3-7. 사용자1의 댓글 생성에 필요한 내용 준비
// 3-8. 사용자1의 댓글 생성
// when
// 3-9. 사용자2가 사용자1의 댓글 수정 시도
// then
// 검증
}
Java물론, 테스트들 간에 완전히 똑같이 필요한 데이터가 반복된다면, 그 부분은 @BeforeEach로 추출해 중복을 줄이는 방법도 있습니다. 그러나 “테스트별로 크게 달라지는 시나리오”일 땐, 위와 같은 독립 구성 방식이 더 직관적이고 유연하다고 판단했습니다.
테스트 코드를 작성할 때, Mock 및 Spy 개념을 적절히 활용하면 실제 외부 의존성 없이도 독립적이고 유연하게 테스트를 진행할 수 있습니다.
결국, 테스트 설계의 핵심은 “테스트하고자 하는 대상 로직을 최대한 명확히 드러내면서, 불필요한 의존성을 제거하여 빠르고 안정적인 검증을 가능케 하는 것”이라 생각합니다.
위의 내용을 바탕으로, 본인 프로젝트나 업무 환경에 맞춰 다양한 방식의 테스트 전략을 시도해 보시길 바랍니다. “Practical Testing: 실용적인 테스트 가이드” 강의와 “인프런 워밍업 클럽 스터디 3기”에서 다뤘던 내용을 공유했는데, 실제로 코드에 적용해보면 테스트 품질과 개발 효율이 한층 높아질 것이라 생각합니다.
들어가며 스프링 기반 프로젝트에서 좋은 설계 구조와 테스트 전략은 소프트웨어 품질과 유지보수성에 직결됩니다. 최근 학습한…
들어가며 코드를 작성할 때 종종 "이 로직을 어떻게 단순하고 읽기 쉽게 표현할 수 있을까?" 고민하게…
HTTP 상태코드란 무엇인가? HTTP 상태코드(HTTP Status Code)는 서버가 클라이언트의 요청을 처리한 결과를 수치화된 코드로 나타내는…
HTTP란 무엇인가? HTTP(Hypertext Transfer Protocol)는 웹에서 데이터를 주고받기 위해 사용하는 응용 계층 프로토콜입니다. 우리가 브라우저에서…
HTTP란 무엇인가? HTTP(Hypertext Transfer Protocol)는 인터넷에서 웹 브라우저와 웹 서버가 서로 통신하기 위해 사용하는 프로토콜입니다.…