요약
Spring Boot 테스트, JUnit & Mockito 완벽 가이드 2026
2026년, 안정적인 Spring Boot 백엔드 서비스를 위한 테스트 코드 작성은 이제 선택이 아닌 필수입니다. 이 가이드를 통해 JUnit과 Mockito를 활용한 단위 테스트 및 통합 테스트 작성법을 권퓨터와 함께 마스터하세요.
핵심 키워드: Spring Boot 테스트, JUnit, Mockito
목차
1. 왜 Spring Boot 테스트가 중요할까요?
2. JUnit 5, 테스트의 기본기를 다지다
3. Mockito, 가짜 객체로 진짜 테스트하기
4. Spring Boot와 함께하는 통합 테스트
5. 실전! 계층별 테스트 전략과 예시
6. 테스트 효율을 높이는 고급 기법과 팁
7. 흔히 겪는 문제와 해결책
8. 마치며: 2026년, 더 나은 서비스를 향한 여정
1. 왜 Spring Boot 테스트가 중요할까요?
안녕하세요, 권퓨터입니다! 오늘은 Spring Boot 개발자라면 누구나 공감할 만한, 아니 반드시 알아야 할 중요한 주제를 들고 왔습니다. 바로 ‘테스트’입니다. 특히 Spring Boot 애플리케이션에서 안정성과 신뢰성을 확보하기 위한 핵심 도구인 JUnit과 Mockito를 활용한 테스트 전략을 2026년 최신 관점에서 심층 분석해 볼 예정입니다.
빠르게 변화하는 IT 환경에서 서비스의 복잡성은 날마다 증가하고 있습니다. 작은 코드 변경 하나가 전체 시스템에 예상치 못한 영향을 미칠 수 있죠. 이러한 위험을 최소화하고, 개발 생산성을 극대화하며, 궁극적으로 사용자에게 고품질의 서비스를 제공하기 위해선 체계적인 테스트가 필수적입니다. 특히 Spring Boot의 마이크로서비스 아키텍처나 클라우드 네이티브 환경에서는 각 컴포넌트의 독립적인 검증이 더욱 중요해집니다.
핵심 포인트
2026년의 Spring Boot 개발에서 테스트는 더 이상 선택 사항이 아닌, 서비스의 안정성과 개발 효율을 위한 필수 요소입니다. 특히 JUnit과 Mockito는 이 여정의 핵심 도구입니다.
테스트가 필요한 이유: 비용 절감과 품질 향상
많은 개발자들이 “테스트 코드를 작성할 시간이 없다”고 말하지만, 사실 테스트는 장기적으로 개발 비용을 절감하는 가장 효과적인 방법입니다. 버그가 프로덕션 환경에서 발견되면 수정 비용은 개발 단계에서 발견될 때보다 훨씬 커집니다. 예를 들어, 2023년 Stack Overflow 설문조사에 따르면, 개발자의 약 50%가 디버깅에 주당 5시간 이상을 소비한다고 합니다. 이는 테스트가 부족할 때 발생하는 숨겨진 비용이죠.
테스트는 다음과 같은 이점을 제공합니다:
장점
✓ 초기 버그 발견: 개발 주기 초기에 문제를 파악하여 수정 비용을 최소화합니다.
✓ 코드 품질 향상: 테스트 가능한 코드를 작성하려면 자연스럽게 모듈화되고 응집도 높은 코드를 설계하게 됩니다.
✓ 리팩토링 자신감: 기존 기능이 손상되지 않음을 보장하며 코드를 개선할 수 있습니다.
✓ 문서화 역할: 테스트 코드는 기능의 의도를 가장 정확하게 설명하는 문서 역할을 합니다.
✓ 협업 효율 증대: 팀원들이 서로의 코드를 더 쉽게 이해하고 변경할 수 있게 합니다.
2. JUnit 5, 테스트의 기본기를 다지다
JUnit은 자바 개발 환경에서 가장 널리 사용되는 테스트 프레임워크입니다. 특히 JUnit 5는 모듈화된 아키텍처와 강력한 기능을 제공하여 현대적인 테스트 요구사항을 충족시킵니다. Spring Boot는 JUnit 5를 기본 테스트 라이브러리로 포함하고 있어, 별도의 설정 없이 바로 사용할 수 있습니다.

JUnit 5의 주요 특징
JUnit 5 핵심 요소
JUnit Platform — JVM에서 테스트 프레임워크를 실행하기 위한 기반입니다.
JUnit Jupiter — JUnit 5의 새로운 프로그래밍 모델이자 확장 모델을 제공합니다. 대부분의 테스트 코드는 Jupiter API를 사용합니다.
JUnit Vintage — JUnit 3, 4 기반의 기존 테스트를 JUnit 5 플랫폼에서 실행할 수 있게 해줍니다.
기본 테스트 작성법: @Test와 @DisplayName
가장 기본적인 JUnit 테스트는 @Test 어노테이션이 붙은 메서드입니다. 이 메서드는 특정 코드 조각이 예상대로 동작하는지 검증합니다. @DisplayName을 사용하면 테스트 메서드에 더 가독성 높은 이름을 부여할 수 있어 테스트 리포트 확인 시 매우 유용합니다.
코드 설명
간단한 계산기 클래스의 덧셈 기능을 테스트하는 JUnit 5 코드입니다. @DisplayName으로 테스트의 목적을 명확히 합니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class Calculator {
int add(int a, int b) {
return a + b;
}
}
class CalculatorTest {
// @DisplayName을 사용하여 테스트 이름 명확화
@DisplayName("덧셈 테스트: 양수 두 개 더하기")
@Test
void testAddPositiveNumbers() {
Calculator calculator = new Calculator();
int result = calculator.add(5, 3);
// 예상 값과 실제 값이 같은지 검증
assertEquals(8, result, "5 + 3은 8이어야 합니다.");
}
@DisplayName("덧셈 테스트: 음수와 양수 더하기")
@Test
void testAddNegativeAndPositiveNumber() {
Calculator calculator = new Calculator();
int result = calculator.add(-5, 3);
assertEquals(-2, result, "-5 + 3은 -2이어야 합니다.");
}
}
테스트 라이프사이클 관리: @BeforeEach, @AfterEach
테스트를 실행하기 전후에 특정 작업을 수행해야 할 때가 많습니다. 예를 들어, 각 테스트 메서드가 독립적으로 실행되도록 객체를 초기화하거나, 테스트 후 리소스를 정리하는 등의 작업이죠. JUnit 5는 이를 위한 어노테이션을 제공합니다.
테스트 라이프사이클 어노테이션
각 테스트 메서드 실행 전후에 수행되는 작업 정의
@BeforeEach: 각 테스트 메서드 실행 직전에 한 번씩 실행됩니다. (예: 테스트 객체 초기화)
@AfterEach: 각 테스트 메서드 실행 직후에 한 번씩 실행됩니다. (예: 리소스 정리)
핵심 포인트
@BeforeEach는 각 테스트의 독립성을 보장하는 데 필수적입니다. 테스트 간의 의존성을 제거하여 테스트 결과를 예측 가능하게 만듭니다.
3. Mockito, 가짜 객체로 진짜 테스트하기
단위 테스트의 핵심은 테스트 대상 코드(System Under Test, SUT)를 다른 부분으로부터 격리하여 테스트하는 것입니다. 하지만 SUT가 다른 객체(의존성)에 의존하는 경우가 대부분이죠. 이때 Mockito가 등장합니다. Mockito는 가짜 객체(Mock Object)를 생성하여 SUT의 의존성을 대체하고, 특정 상황에서 의존성이 어떻게 동작해야 하는지 정의할 수 있게 해주는 강력한 프레임워크입니다.
Mockito를 사용하면 데이터베이스 접근, 외부 API 호출 등 실제 환경에서 시간이 오래 걸리거나 불안정한 요소를 모킹(Mocking)하여 빠르고 안정적인 단위 테스트를 구현할 수 있습니다. 2026년에도 Mockito는 자바 백엔드 테스트에서 독보적인 위치를 차지하고 있습니다.

@Mock, @InjectMocks의 마법
Mockito의 핵심 어노테이션은 @Mock과 @InjectMocks입니다. 이 둘을 사용하면 보일러플레이트 코드 없이 테스트 객체를 쉽게 설정할 수 있습니다.
Mockito 핵심 어노테이션
가짜 객체 생성 및 주입을 위한 편리한 도구
@Mock: 가짜 객체(Mock)를 생성합니다. 이 객체는 실제 객체처럼 보이지만, 실제 로직을 실행하지 않고 우리가 정의한 대로 동작합니다.
@InjectMocks: @Mock으로 생성된 가짜 객체들을 자동으로 주입받는 테스트 대상 객체를 생성합니다. 생성자, 필드, 세터 주입 방식 모두 지원합니다.
when().thenReturn(): 가짜 객체 동작 정의
when(mockObject.method()).thenReturn(expectedValue) 구문은 Mockito의 핵심입니다. 이 구문을 통해 가짜 객체의 특정 메서드가 호출될 때 어떤 값을 반환할지 미리 정의할 수 있습니다. 이를 스터빙(Stubbing)이라고 합니다.
코드 설명
사용자 정보를 관리하는 서비스(UserService)를 테스트합니다. UserRepository는 실제 데이터베이스에 접근하지 않고 Mockito로 모킹합니다.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
// JUnit 5에서 Mockito 어노테이션을 사용하기 위한 필수 확장
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
// UserRepository 인터페이스를 모킹
@Mock
private UserRepository userRepository;
// Mock 객체들을 UserService에 주입
@InjectMocks
private UserService userService;
// 테스트에 사용될 가상의 User 객체
private User testUser;
@BeforeEach
void setUp() {
testUser = new User(1L, "kwonputer", "[email protected]");
}
@DisplayName("사용자 ID로 사용자 조회 테스트")
@Test
void testGetUserById() {
// userRepository.findById(1L)이 호출될 때 testUser를 반환하도록 설정
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
User foundUser = userService.getUserById(1L);
assertEquals("kwonputer", foundUser.getUsername());
assertEquals("[email protected]", foundUser.getEmail());
// userRepository.findById(1L) 메서드가 1번 호출되었는지 검증
verify(userRepository, times(1)).findById(1L);
}
}
// 가상의 User 클래스
class User {
private Long id;
private String username;
private String email;
public User(Long id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}
public Long getId() { return id; }
public String getUsername() { return username; }
public String getEmail() { return email; }
}
// 가상의 UserRepository 인터페이스
// @Repository
interface UserRepository {
Optional<User> findById(Long id);
User save(User user);
}
핵심 포인트
Mockito의 when().thenReturn()은 Mock 객체가 특정 호출에 대해 어떻게 응답할지 정의하는 핵심 기능입니다. 이를 통해 실제 의존성 없이도 SUT의 동작을 정확하게 테스트할 수 있습니다.
4. Spring Boot와 함께하는 통합 테스트
단위 테스트가 개별 컴포넌트의 동작을 검증한다면, 통합 테스트는 여러 컴포넌트가 함께 작동할 때의 상호작용을 검증합니다. Spring Boot는 통합 테스트를 매우 쉽게 작성할 수 있도록 다양한 어노테이션과 유틸리티를 제공합니다.
Spring Boot 테스트의 가장 큰 장점은 실제 애플리케이션 컨텍스트를 부분적으로 또는 전체적으로 로드하여 테스트할 수 있다는 점입니다. 이를 통해 실제 운영 환경과 유사한 조건에서 테스트를 수행할 수 있으며, Spring의 의존성 주입(DI) 컨테이너가 제대로 작동하는지 확인할 수 있습니다.

@SpringBootTest: 풀 컨텍스트 통합 테스트
@SpringBootTest는 Spring Boot 애플리케이션의 전체 컨텍스트를 로드하여 통합 테스트를 수행할 때 사용합니다. 이는 실제 애플리케이션이 실행되는 방식과 가장 유사한 환경을 제공합니다. 애플리케이션의 모든 빈(Bean)이 로드되므로, 서비스 계층, 데이터베이스 계층, 웹 계층 등 전체 스택이 올바르게 통합되는지 확인할 수 있습니다.
주의할 점은 전체 컨텍스트를 로드하기 때문에 테스트 실행 시간이 길어질 수 있다는 것입니다. 따라서 꼭 필요한 경우에만 사용하는 것이 좋습니다. 일반적으로는 컨트롤러, 서비스, 리포지토리 등 특정 계층만 테스트하는 “슬라이스 테스트”를 선호합니다.
코드 설명
@SpringBootTest를 사용하여 전체 Spring 컨텍스트를 로드하고, TestRestTemplate을 통해 실제 HTTP 요청을 보내는 통합 테스트 예시입니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
// 실제 웹 환경을 시뮬레이션하고 랜덤 포트에서 실행
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HelloControllerIntegrationTest {
// 랜덤으로 할당된 포트 주입
@LocalServerPort
private int port;
// HTTP 요청을 보내기 위한 유틸리티
@Autowired
private TestRestTemplate restTemplate;
@DisplayName("HelloController의 /hello 엔드포인트 테스트")
@Test
void testHelloEndpoint() {
String baseUrl = "http://localhost:" + port + "/hello";
ResponseEntity<String> response = restTemplate.getForEntity(baseUrl, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("Hello, Spring Boot!", response.getBody());
}
}
// 가상의 Spring Boot 애플리케이션 클래스 (실제 프로젝트에서는 @SpringBootApplication 사용)
// 아래는 테스트를 위한 최소한의 예시입니다.
// 실제로는 메인 애플리케이션 클래스에 @SpringBootApplication이 붙습니다.
// @SpringBootApplication
// public class DemoApplication {
// public static void main(String[] args) {
// SpringApplication.run(DemoApplication.class, args);
// }
// }
// 가상의 HelloController 클래스 (실제 프로젝트에서는 @RestController 사용)
// @RestController
// public class HelloController {
// @GetMapping("/hello")
// public String hello() {
// return "Hello, Spring Boot!";
// }
// }
@WebMvcTest, @DataJpaTest: 슬라이스 테스트로 효율 높이기
@SpringBootTest의 단점을 보완하기 위해 Spring Boot는 특정 계층만 테스트할 수 있는 “슬라이스 테스트(Slice Test)” 어노테이션을 제공합니다. 이 어노테이션들은 필요한 빈만 로드하여 테스트 실행 시간을 단축하고 테스트의 초점을 명확히 합니다.
Spring Boot 슬라이스 테스트
@WebMvcTest — 웹 계층(Controller) 테스트에 특화되어 있습니다. @Controller, @RestController, @ControllerAdvice 등 웹 관련 빈만 로드하며, 서비스나 리포지토리 계층은 모킹하여 사용합니다. MockMvc를 통해 HTTP 요청을 시뮬레이션합니다.
@DataJpaTest — JPA 데이터 액세스 계층(Repository) 테스트에 특화되어 있습니다. @Repository 빈과 인메모리 데이터베이스(H2 등)를 자동으로 구성하여 실제 데이터베이스 없이 빠르고 안정적인 테스트를 가능하게 합니다.
@JsonTest, @RestClientTest 등 다양한 슬라이스 테스트 어노테이션이 존재합니다.
핵심 포인트
@SpringBootTest는 전체 통합 테스트에 적합하지만, 테스트 속도를 위해 특정 계층만 테스트하는 @WebMvcTest나 @DataJpaTest와 같은 슬라이스 테스트를 적극 활용하는 것이 좋습니다.
5. 실전! 계층별 테스트 전략과 예시
이제 이론을 바탕으로 Spring Boot 애플리케이션의 각 계층별로 어떻게 테스트를 작성해야 하는지 구체적인 전략과 예시를 살펴보겠습니다. 일반적으로 Spring Boot 애플리케이션은 Controller, Service, Repository 계층으로 구성됩니다.
5.1. Repository 계층 테스트: @DataJpaTest
Repository 계층은 데이터베이스와의 상호작용을 담당합니다. @DataJpaTest는 이 계층을 테스트하기에 최적화된 어노테이션입니다. 인메모리 데이터베이스를 사용하고 JPA 관련 빈만 로드하여 실제 데이터베이스 연결 없이 빠르게 테스트할 수 있습니다.
코드 설명
@DataJpaTest를 사용하여 UserRepository의 저장 및 조회 기능을 테스트합니다. TestEntityManager를 통해 직접 데이터를 조작할 수 있습니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
// JPA 관련 빈만 로드하고 인메모리 DB 설정
@DataJpaTest
class UserRepositoryTest {
// 영속성 컨텍스트를 직접 관리하는 유틸리티
@Autowired
private TestEntityManager entityManager;
// 테스트 대상 Repository 주입
@Autowired
private UserRepository userRepository;
@DisplayName("사용자 저장 및 조회 테스트")
@Test
void testSaveAndFindUser() {
User user = new User("권퓨터", "[email protected]");
entityManager.persist(user); // DB에 사용자 저장
entityManager.flush(); // 변경사항 즉시 반영
Optional<User> foundUser = userRepository.findByUsername("권퓨터");
assertTrue(foundUser.isPresent());
assertEquals("[email protected]", foundUser.get().getEmail());
}
@DisplayName("사용자 ID로 조회 실패 테스트")
@Test
void testFindUserByIdNotFound() {
Optional<User> foundUser = userRepository.findById(999L); // 존재하지 않는 ID
assertTrue(foundUser.isEmpty());
}
}
// 가상의 User 엔티티 (실제 프로젝트에서는 @Entity, @Id 등 사용)
// @Entity
// @Table(name = "users")
class User {
// @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
public User() {}
public User(String username, String email) {
this.username = username;
this.email = email;
}
// Getters and setters (생략)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
// 가상의 UserRepository 인터페이스 (실제 프로젝트에서는 JpaRepository 상속)
// @Repository
interface UserRepository extends org.springframework.data.jpa.repository.JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
5.2. Service 계층 테스트: JUnit & Mockito
Service 계층은 비즈니스 로직을 담당합니다. 이 계층은 주로 Repository나 다른 Service에 의존하므로, Mockito를 사용하여 의존성을 모킹하고 순수하게 서비스의 로직만을 테스트하는 것이 단위 테스트의 모범 사례입니다. @ExtendWith(MockitoExtension.class)와 @InjectMocks, @Mock 어노테이션을 활용합니다.
코드 설명
이전 섹션의 UserService와 UserRepository 예시를 확장하여, 사용자 생성 로직을 테스트합니다. userRepository.save() 메서드를 모킹하여 실제 데이터베이스 호출 없이 서비스 로직을 검증합니다.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class)
class UserServiceCreateTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
private User newUser;
@BeforeEach
void setUp() {
newUser = new User("새로운권퓨터", "[email protected]");
}
@DisplayName("새로운 사용자 생성 테스트")
@Test
void testCreateUser() {
// userRepository.save()가 어떤 User 객체를 받든 그대로 반환하도록 설정
when(userRepository.save(any(User.class))).thenReturn(newUser);
User createdUser = userService.createUser(newUser);
assertNotNull(createdUser);
assertEquals("새로운권퓨터", createdUser.getUsername());
assertEquals("[email protected]", createdUser.getEmail());
// userRepository.save() 메서드가 1번 호출되었는지 검증
verify(userRepository, times(1)).save(any(User.class));
}
}
5.3. Controller 계층 테스트: @WebMvcTest와 MockMvc
Controller 계층은 HTTP 요청을 처리하고 응답을 반환하는 역할을 합니다. @WebMvcTest는 이 계층의 테스트에 최적화되어 있으며, MockMvc 객체를 통해 실제 HTTP 요청을 보내는 것처럼 컨트롤러의 동작을 테스트할 수 있습니다. 서비스 계층은 @MockBean을 사용하여 모킹합니다.
코드 설명
@WebMvcTest를 사용하여 UserController를 테스트합니다. UserService는 @MockBean으로 모킹하여 실제 서비스 로직이 아닌 가짜 객체를 사용하도록 설정합니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
// 웹 계층 테스트에 특화된 어노테이션 (UserController만 로드)
@WebMvcTest(UserController.class)
class UserControllerTest {
// HTTP 요청 시뮬레이션을 위한 MockMvc 객체
@Autowired
private MockMvc mockMvc;
// UserService 빈을 모킹하여 주입
@MockBean
private UserService userService;
@DisplayName("GET /users/{id} 엔드포인트 테스트")
@Test
void testGetUserById() throws Exception {
User user = new User(1L, "권퓨터", "[email protected]");
// userService.getUserById(1L)이 호출될 때 user 객체를 반환하도록 모킹
when(userService.getUserById(1L)).thenReturn(user);
mockMvc.perform(get("/users/{id}", 1L)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) // HTTP 200 OK 예상
.andExpect(jsonPath("$.username").value("권퓨터")) // JSON 응답 검증
.andExpect(jsonPath("$.email").value("[email protected]"));
}
}
// 가상의 UserController 클래스 (실제 프로젝트에서는 @RestController 등 사용)
// @RestController
// @RequestMapping("/users")
class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// @GetMapping("/{id}")
public User getUserById(Long id) {
return userService.getUserById(id);
}
}
핵심 포인트
Spring Boot의 테스트 전략은 테스트 피라미드에 기반합니다. 가장 많은 단위 테스트(Service 계층), 그 다음 슬라이스 테스트(Repository/Controller), 가장 적은 통합 테스트(@SpringBootTest) 순으로 작성하여 효율성과 안정성을 동시에 잡아야 합니다.

6. 테스트 효율을 높이는 고급 기법과 팁
JUnit과 Mockito의 기본 사용법을 넘어, 테스트 코드를 더 견고하고 효율적으로 만드는 몇 가지 고급 기법과 팁을 소개합니다. 2026년의 개발 환경에서는 이러한 기법들이 더욱 중요해지고 있습니다.
6.1. 파라미터화된 테스트: @ParameterizedTest
동일한 테스트 로직을 여러 다른 입력 값으로 반복해서 실행해야 할 때가 있습니다. 예를 들어, 유효성 검사 로직을 테스트할 때 다양한 유효/유효하지 않은 입력 값을 시도해야 하죠. JUnit 5의 @ParameterizedTest는 이런 경우에 매우 유용합니다.
파라미터 소스 어노테이션
@ParameterizedTest와 함께 사용되는 데이터 소스
@ValueSource: 기본형 타입(String, int, long 등)의 단일 인자 값을 제공합니다.
@CsvSource: 콤마로 구분된 문자열을 여러 인자 값으로 제공합니다.
@MethodSource: 테스트 클래스 내의 정적 메서드가 반환하는 Stream을 통해 복잡한 객체나 여러 인자 값을 제공합니다.
@CsvFileSource: CSV 파일에서 테스트 데이터를 읽어옵니다.
6.2. Test Doubles: 스텁, 스파이, 페이크
Mockito는 Mock 객체를 생성하지만, 테스트 더블(Test Double)에는 Mock 외에도 스텁(Stub), 스파이(Spy), 페이크(Fake) 등 다양한 종류가 있습니다. 각자의 역할과 사용 시점을 이해하는 것이 중요합니다.
테스트 더블의 종류
Mock (가짜 객체) — 특정 메서드 호출 시 미리 정의된 값을 반환하고, 호출 여부/횟수를 검증합니다. 행위 검증에 중점을 둡니다. (Mockito의 기본 기능)
Stub (대역) — 특정 메서드 호출 시 미리 정의된 값을 반환합니다. 상태 기반 테스트에 주로 사용됩니다. (Mockito의 when().thenReturn()으로 구현 가능)
Spy (감시 객체) — 실제 객체의 메서드를 호출하되, 일부 메서드의 동작만 변경하거나 호출 정보를 기록합니다. 부분적인 모킹이 필요할 때 사용합니다. (Mockito의 @Spy 어노테이션)
Fake (가짜 구현) — 실제 객체와 유사하게 동작하지만, 단순화된 구현을 제공합니다. (예: 실제 DB 대신 인메모리 DB)
6.3. TDD (Test-Driven Development) 실천
테스트 주도 개발(TDD)은 “실패하는 테스트를 먼저 작성하고, 그 테스트를 통과시키는 최소한의 코드를 작성한 다음, 코드를 리팩토링하는” 개발 방법론입니다. TDD는 코드 설계 개선, 버그 감소, 개발 생산성 향상에 큰 도움을 줍니다.
Step 1
빨강 (Red) 단계
새로운 기능에 대한 실패하는 테스트 코드를 먼저 작성합니다. 아직 기능 구현이 안 되었으므로 당연히 실패합니다.
Step 2
초록 (Green) 단계
작성한 테스트를 통과시키는 최소한의 기능 코드를 작성합니다. 오직 테스트를 통과시키는 데 집중합니다.
Step 3
리팩토링 (Refactor) 단계
테스트가 모두 통과하는 상태에서 코드의 중복을 제거하고, 가독성을 높이며, 설계를 개선합니다. 테스트 코드가 있으므로 안전하게 리팩토링할 수 있습니다.
핵심 포인트
TDD는 단순히 테스트를 작성하는 것을 넘어, 설계 프로세스의 일부입니다. 테스트 코드를 먼저 작성함으로써 더 명확하고 테스트 가능한 코드를 만들 수 있습니다.