소프트웨어 개발에서 테스트 코드는 코드의 안정성을 높이고, 예상대로 동작하는지 검증하는 중요한 과정입니다.
특히, Java에서는 JUnit이 대표적인 단위 테스트(Unit Test) 프레임워크로 널리 사용됩니다.
하지만, 테스트 대상 객체가 데이터베이스, 네트워크, 외부 API 등 복잡한 의존성을 가질 경우 직접 테스트하는 것이 어려울 수 있습니다.
이때 Mocking(목킹) 기법을 활용하면 실제 객체를 대체하는 가짜(Mock) 객체를 생성하여 독립적인 테스트가 가능해집니다.
이번 글에서는 JUnit 5와 Mockito를 활용하여 단위 테스트를 효율적으로 작성하는 방법을 정리해보겠습니다.
JUnit5 이란?
JUnit 5는 세 개의 하위 프로젝트에서 제공하는 여러 개의 모듈로 구성됩니다.
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform : JVM에서 테스트 프레임워크를 실행하기 위한 기반 플랫폼 역할
JUnit Jupiter : JUnit 5의 주요 테스트 실행 엔진 TestEngine을 제공
JUnit Vintage : 기존의 JUnit 3,4 테스트를 JUnit 5에서 실행할 수 있도록 지원하는 엔진
JUnit5의 주요 어노테이션
| 어노테이션 | 설명 |
| @Test | 테스트 메서드를 나타냄 |
| @BeforeEach | 각 테스트 실행 전에 실행됨 |
| @AfterEach | 각 테스트 실행 후에 실행됨 |
| @BeforeAll | 전체 테스트 실행 전에 한 번 실행됨 (static 메서드 필요) |
| @AfterAll | 전체 테스트 실행 후 한 번 실행됨 (static 메서드 필요) |
| @DisplayName("테스트 설명") | 테스트의 가독성을 높이기 위한 설명 추가 |
| @Disabled | 특정 테스트를 비활성화 |
JUnit5 예제 코드
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class Test {
@BeforeAll
static void setup() {
System.out.println("모든 테스트 실행 전 (한 번만 실행)");
}
@BeforeEach
void init() {
System.out.println("각 테스트 실행 전");
}
@Test
@DisplayName("1 + 1 = 2")
void additionTest() {
int result = 1 + 1;
assertEquals(2, result, "1 + 1은 2여야 한다.");
}
@AfterEach
void tearDown() {
System.out.println("각 테스트 실행 후");
}
@AfterAll
static void done() {
System.out.println("모든 테스트 실행 완료 (한 번만 실행)");
}
}
실행 순서
- @BeforeAll → @BeforeEach → @Test → @AfterEach
- 여러 개의 테스트가 있다면 각 테스트마다 @BeforeEach → @Test → @AfterEach 반복
- 모든 테스트가 끝난 후 @AfterAll 실행
Mock이란?
사전에서 mock이라는 단어를 찾아보면, 무언가를 흉내 내어 만든 것이라는 정의를 찾을 수 있습니다.
Mocking은 단위테스트에 주로 사용됩니다.
테스트하려는 객체는 다른 복잡한 객체에 의존할 수 있습니다. 이때, 테스트 대상 객체의 동작을 독립적으로 테스트하기 위해 실체 객체 대신 Mock 객체를 사용합니다.
Mockito 라이브러리를 사용하여 Mock 객체를 쉽게 생성한다.
Mock 객체를 사용하는 이유
- 실제 객체를 테스트 환경에 포함시키기 어려울 때
- 테스트 수행 속도를 높이기 위해
- 네트워크, 데이터베이스 등 외부 의존성을 제거하여 테스트의 신뢰성을 높이기 위해
👉 즉, 목킹(Mocking)이란 실제 객체의 동작을 모방하는 객체를 생성하는 과정입니다.
Mockito의 주요 어노테이션
| 어노테이션 | 설명 |
| @Mock | 가짜(Mock) 객체 생성 |
| @InjectMocks | 실제 객체를 생성하면서, 내부 의존성을 @Mock으로 주입 |
| @Spy | 실제 객체를 감싸서 일부 메서드는 Mock 처리하고 일부는 실제 동작 |
| @Captor | ArgumentCaptor를 활용하여 메서드 호출 시 전달된 인자를 검증 |
| @MockBean | Spring Boot 환경에서 Mock 객체를 Spring Bean으로 등록 |
Mocktio 예제 코드
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;
import java.util.List;
class Test {
@Test
void mockListTest() {
// Mock 객체 생성
List<String> mockList = Mockito.mock(List.class);
// 동작 지정: 특정 인덱스에서 반환할 값 설정
when(mockList.get(0)).thenReturn("Hello, Mockito!");
// 검증
assertEquals("Hello, Mockito!", mockList.get(0));
assertNull(mockList.get(1)); // 별도 설정하지 않으면 기본적으로 null 반환
// 특정 메서드가 호출되었는지 확인
verify(mockList).get(0);
}
}
JUnit5 + Mockito를 활용한 테스트 코드 작성
Spring Boot 환경에서 서비스 테스트할 때, @MockBean을 활용하면 유용합니다.
Spring Boot 환경에서 Mock을 이용한 서비스 테스트
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository; // 가짜 Repository 객체 생성
@Mock
private BCryptPasswordEncoder bCryptPasswordEncoder; //가짜 비밀번호 암호화 객체 생성
@InjectMocks
private UserService userService; // Mock 객체들이 주입된 실제 테스트 대상 서비스
private SignUpDto signUpDto;
@BeforeEach
void setUp() {
signUpDto = new SignUpDto();
signUpDto.setUsername("testUser");
signUpDto.setPassword("password123");
}
@Test
@DisplayName("회원가입 성공")
void signUp_Success() {
//given - 가짜 동작 설정
when(userRepository.existsByUsername(any(String.class))).thenReturn(false);
when(bCryptPasswordEncoder.encode(any(String.class))).thenReturn("encodedPassword");
//when - 회원가입 실행
userService.signUp(signUpDto);
//then
verify(userRepository, times(1)).existsByUsername("testUser");
verify(bCryptPasswordEncoder, times(1)).encode("password123");
verify(userRepository, times(1)).save(any(User.class));
}
@Test
@DisplayName("중복된 아이디 검증")
void signUp_DuplicateUsername_ThrowsException() {
//given - 중복된 아이디가 있다고 설정
when(userRepository.existsByUsername(any(String.class))).thenReturn(true);
//when & then
Exception exception = assertThrows(IllegalArgumentException.class, () -> userService.signUp(signUpDto));
assertEquals("이미 사용중인 아이디입니다.", exception.getMessage());
verify(userRepository, times(1)).existsByUsername("testUser");
verify(userRepository, never()).save(any(User.class)); //save()는 호출되지 않아야함
}
}
- @Mock → 가짜 데이터 저장소(UserRepository)를 생성
- @InjectMocks → UserService에 @Mock이 주입됨
Mocking vs Stubbing
때때로 Mocking(목킹)과 Stubbing(스터빙)을 구분해야 할 때가 있습니다.
이 두 개념에 대해 다음과 같이 정의할 수 있습니다.
Stub (데이터베이스 대체)
- 최소한의 기능만 제공하는 단순한 가짜 객체
- 테스트가 실행될 수 있도록 필요한 동작만 구현
- 테스트에서 기대하는 동작을 흉내 내는 역할 (결과값 반환 등)
Mock (데이터베이스 호출 검증)
- Stub과 유사하지만, 추가적으로 "테스트에서 해당 객체가 올바르게 사용되었는지 확인"
- 테스트가 끝난 후 Mock 객체가 예상대로 호출되었는지를 검증하는 것
예를 들어, 데이터베이스를 Stub으로 구현하면 메모리에 간단한 구조로 레코드를 저장할 수 있습니다. 테스트 대상 객체는 이 Stub을 통해 데이터를 읽고 쓸 수 있으며, 이를 활용해 데이터베이스와 무관한 동작을 검증할 수 있습니다. 즉, 데이터베이스 Stub은 단순히 테스트 실행을 위한 대체 객체로 사용됩니다.
반면, 테스트 대상 객체가 특정 데이터를 데이터베이스에 저장하는지 검증하려면, 데이터베이스를 Mock으로 만들어야 합니다. 이 경우 테스트에서는 Mock 객체에 올바른 데이터가 기록되었는지를 검증하는 어설션(assertion)을 포함하게 됩니다.
참고 자료
'공부방' 카테고리의 다른 글
| 인덱스의 중요성, 성능 테스트로 증명하기 (2) | 2025.08.13 |
|---|---|
| CQRS 패턴의 진정한 가치: 부하 테스트로 증명하기 (2) | 2025.08.12 |
| EC2 + Docker + GitHub Actions + Nginx + Route 53 기반 프론트 배포 & HTTPS 적용기 (0) | 2025.04.12 |
| EC2 + Docker + GitHub Actions Spring Boot 배포 회고 (0) | 2025.03.23 |
| WebSockets vs SSE (0) | 2025.02.17 |