Pink Spider/Test Double

Created Thu, 03 Apr 2025 15:57:29 +0900 Modified Mon, 08 Dec 2025 08:41:47 +0900
1043 Words 5 min

테스트 더블(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 연결 없이도 로직을 검증하고 싶을 때 아주 유용합니다.


🧩 예제 시나리오: UserServiceUserRepository를 이용해 유저를 조회하는 서비스


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 설정해서 쓰는 편입니다.