테스트 더블(Test Double) 용어 정리
- 아래는 테스트에서 자주 쓰이는 5가지 테스트 대역입니다:
🧪 테스트 더블(Test Double) 종류 요약
| 타입 |
설명 |
주로 하는 일 |
대표 예시 |
| Dummy |
값이 필요하지만 아무 역할도 하지 않는 객체 |
메서드 시그니처 채우기 용 |
new User(null)처럼 값만 채움 |
| Fake |
실제 구현에 가까우나, 단순하거나 in-memory 기반 |
간단한 대체 구현 |
InMemory DB, FakeEmailSender |
| Stub |
고정된 응답을 리턴하는 객체 |
상태 기반 테스트 |
when(repo.findById()).thenReturn() |
| Mock |
행위 기반, 호출 여부나 순서 검증이 가능한 객체 |
행위 기반 테스트 |
verify(repo).save() |
| Spy |
실제 객체를 감싸면서, 부분적으로 mock 가능 |
실제 동작 + 호출 감시 |
spy(new UserService(...)) |
1. 🐣 Dummy
- 가장 단순한 형태.
- 아무 일도 하지 않고, 값만 채우는 용도.
- 테스트에 직접적 의미 없음. 없어도 테스트 통과 가능.
User dummyUser = new User(null); // 생성자 매개변수 맞추려고 넣는 용도
2. 🧪 Stub
- 호출하면 정해진 값을 리턴함.
- 동작 자체는 단순하고 검증 로직은 없음.
- 상태 기반 테스트에 사용됨.
when(userRepository.findById(1L)).thenReturn(Optional.of(new User("Alice")));
3. 🤖 Mock
- Stub처럼 리턴값을 줄 수 있지만, 추가로 호출 여부도 검증함.
- 테스트의 **행위 검증(Behavior Verification)**에 초점.
verify(userRepository, times(1)).save(any());
4. 🕵️ Spy
- 실제 객체를 감싸면서, 필요한 부분만 mock 처리 가능.
- 일부 메서드는 진짜 실행, 일부는 stub/mock 가능.
- 사용 시 조심: 진짜 의존성이 따라올 수 있음.
UserService spyService = spy(new UserService(realRepository));
doReturn("fakeName").when(spyService).getUserName();
5. 🧩 Fake
- 진짜 구현체는 아니지만, 실제로 동작하는 간단한 구현.
- 테스트용으로 가볍게 만든 진짜 비즈니스 로직을 흉내낸 것.
- 예: InMemory DB, 테스트용 메시지 큐 등
public class InMemoryUserRepository implements UserRepository {
private Map<Long, User> db = new HashMap<>();
public Optional<User> findById(Long id) { return Optional.ofNullable(db.get(id)); }
}
🎯 어떤 걸 언제 써야 할까?
| 테스트 목적 |
적합한 더블 |
| 파라미터 채우기만 하면 됨 |
Dummy |
| 고정된 값 리턴만 필요 |
Stub |
| 호출 여부/횟수 검증 |
Mock |
| 실제 객체 + 일부 mocking |
Spy |
| 실제 구현 비슷한 동작 필요 |
Fake |
Fake Repository 예시
Spring 환경에서 자주 쓰이는 Fake Repository 예제를 보여드릴게요.
단위 테스트에서 JPA 같은 실제 DB 연결 없이도 로직을 검증하고 싶을 때 아주 유용합니다.
🧩 예제 시나리오: UserService가 UserRepository를 이용해 유저를 조회하는 서비스
1. 📦 도메인 & 인터페이스 정의
// User.java
public class User {
private Long id;
private String name;
// 생성자, getter, setter 생략
}
// UserRepository.java
public interface UserRepository {
Optional<User> findById(Long id);
void save(User user);
}
2. 🧪 테스트용 Fake 구현체 만들기
// FakeUserRepository.java
public class FakeUserRepository implements UserRepository {
private final Map<Long, User> database = new HashMap<>();
@Override
public Optional<User> findById(Long id) {
return Optional.ofNullable(database.get(id));
}
@Override
public void save(User user) {
database.put(user.getId(), user);
}
// 테스트에서 직접 저장해도 됨
public void clear() {
database.clear();
}
}
3. 🛠️ 서비스 클래스
// UserService.java
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String getUserName(Long id) {
return userRepository.findById(id)
.map(User::getName)
.orElse("Unknown");
}
}
4. ✅ 단위 테스트 코드
// UserServiceTest.java
class UserServiceTest {
private FakeUserRepository fakeUserRepository;
private UserService userService;
@BeforeEach
void setUp() {
fakeUserRepository = new FakeUserRepository();
userService = new UserService(fakeUserRepository);
fakeUserRepository.save(new User(1L, "Alice"));
}
@Test
void testGetUserName() {
String name = userService.getUserName(1L);
assertEquals("Alice", name);
}
@Test
void testGetUserName_NotFound() {
String name = userService.getUserName(2L);
assertEquals("Unknown", name);
}
}
✅ 장점
- Spring 컨텍스트 안 띄워도 됨 (빠름!)
- 실제 비즈니스 로직과 거의 유사하게 테스트 가능
- 의존성 주입도 간단 (생성자 주입)
- 저는 test profile에 h2 설정해서 쓰는 편입니다.