이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Spring 없이 테스트할 수 있는 코드는 어떤 특징을 가지는가?
  • Fake, Stub, Mock은 각각 어떤 상황에서 유용한가?
  • 구현 세부 호출을 과하게 검증하는 테스트가 왜 리팩터링을 방해하는가?

개요

단위 테스트는 Spring 테스트의 반대가 아니다. 오히려 Spring 애플리케이션의 핵심 비즈니스 로직이 프레임워크 없이도 검증 가능하도록 설계되었는지 확인하는 도구다.

좋은 단위 테스트는 빠르고, 실패 원인이 좁고, 리팩터링을 도와준다. 이를 위해 외부 의존성은 테스트 더블로 대체한다. 테스트 더블은 실제 객체 대신 테스트 목적에 맞게 사용하는 객체를 말한다.

테스트 더블 선택

Stub은 정해진 응답을 돌려준다. 외부 API client가 특정 결과를 반환해야 하는 테스트에 적합하다.

Mock은 호출 여부와 인자를 검증한다. 이메일 발송, 메시지 발행처럼 부작용 발생 자체가 중요한 경우에 유용하다.

Fake는 실제 동작을 단순화한 구현체다. in-memory repository처럼 여러 테스트에서 자연스럽게 상태를 다루고 싶을 때 mock 설정보다 읽기 쉽다.

class FakeMemberRepository implements MemberRepository {
 
    private final Map<Long, Member> store = new HashMap<>();
 
    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }
 
    @Override
    public Member save(Member member) {
        store.put(member.id(), member);
        return member;
    }
}

Fake는 테스트의 의도를 코드로 보여 준다. 단, 실제 DB 제약이나 트랜잭션을 검증하지는 않으므로 Repository 쿼리는 별도 slice 테스트가 필요하다.

Fake가 실제 구현처럼 보일수록 경계를 더 조심해야 한다. in-memory fake는 unique constraint, flush timing, lazy loading, transaction isolation 같은 인프라 계약을 재현하지 못한다. fake는 Service 분기와 도메인 규칙을 빠르게 검증하는 도구이고, 인프라 계약은 slice나 통합 테스트가 맡는다.

결과 검증과 상호작용 검증

테스트는 가능하면 결과를 검증한다. 반환값, 상태 변화, 저장된 도메인 객체가 기대와 맞는지를 확인한다. 상호작용 검증은 외부 부작용이나 호출 계약 자체가 중요한 경우로 제한한다.

모든 내부 메서드 호출 순서를 mock으로 고정하면 구현을 바꾸기 어려워진다. 같은 결과를 내는 더 좋은 설계로 리팩터링해도 테스트가 깨지기 때문이다.

코드 예제

class OrderServiceTest {
 
    private final FakeOrderRepository orderRepository = new FakeOrderRepository();
    private final FakeStockRepository stockRepository = new FakeStockRepository();
    private final OrderService orderService = new OrderService(orderRepository, stockRepository);
 
    @Test
    void orderDecreasesStock() {
        stockRepository.save(new Stock(1L, 10));
 
        orderService.placeOrder(new OrderCommand(1L, 2));
 
        assertThat(stockRepository.getQuantity(1L)).isEqualTo(8);
    }
}

이 테스트는 Spring Context 없이 Service 분기를 검증한다. Repository 구현 세부가 아니라 유스케이스 결과를 확인한다.

실전 팁

  • 도메인 객체와 정책 객체는 Spring 없이 테스트할 수 있게 만든다.
  • Mock은 외부 부작용이 핵심일 때만 적극적으로 쓴다.
  • Fake가 반복 mock 설정보다 읽기 쉬우면 Fake를 만든다.
  • 단위 테스트에서 실제 시간, 랜덤, 외부 API를 직접 사용하지 말고 Clock, UUID generator 같은 포트를 둔다.
  • Mock으로 검증할 것은 “이 메시지가 외부로 나가야 한다” 같은 관찰 가능한 부작용으로 좁힌다.
  • 단위 테스트가 어렵다면 생성자 주입과 책임 분리를 먼저 점검한다.

위험 신호!

  • 단위 테스트 하나에 mock 설정이 테스트 본문보다 길다.
  • 내부 private 메서드 호출 순서를 검증하려 한다.
  • 테스트가 결과가 아니라 구현 순서에만 의존한다.
  • Spring Context 없이는 Service를 만들 수 없다.
  • fake repository가 실제 DB 제약까지 보장한다고 착각한다.
  • 단위 테스트가 통과했다는 이유로 query, transaction, serialization 계약 검증을 생략한다.

확인 질문

확인 질문

  • Fake와 Mock의 차이는 무엇인가?
    • Fake는 단순 실제 구현처럼 동작하고, Mock은 호출 여부와 인자를 검증하는 데 초점이 있다.
  • 결과 검증이 상호작용 검증보다 선호되는 이유는 무엇인가?
    • 구현 변경에 덜 취약하고 사용자가 보는 동작을 더 직접적으로 검증하기 때문이다.
  • 단위 테스트가 어려운 코드에서 의심할 설계 문제는 무엇인가?
    • 책임이 크거나 프레임워크와 외부 의존성에 과하게 묶였을 가능성이 있다.

참고 문서