Code/Test

[단위 테스트] 5-1. 목과 테스트 취약성: 목

noahkim_ 2025. 1. 24. 03:56

블라디미르 코리코프 님의 "단위 테스트" 책을 정리한 포스팅입니다.

 

1. Mock vs Stub

  • 테스트 대상 시스템과 그 협력자 사이의 상호 작용을 검사할 수 있는 테스트 대역
구분 Mock (목) Stub (스텁)
역할 외부와의 상호 작용을 모방하고 검증
내부로 들어오는 입력/행위를 모방
목적 협력 객체가 정확히 호출되었는지 협력 객체에서 예상 데이터 제공
방향 Out-bound
In-bound
검증 여부 ✅ 상호작용 검증 (호출 횟수, 순서, 인자 등)
❌ 결과 검증만
적합한 상황 부작용 발생 확인 (Command)
데이터 주입 (Query)
주의사항 과도하면 구현에 종속됨
상호작용 검증은 하지 말 것
테스트 위험 과잉 명세 구현 세부사항 노출
예시 이메일 전송 여부, 결제 API 호출 여부 등 DB에서 회원 조회 결과 반환, 외부 API에서 응답 반환 등

 

CQS 원칙

  • Command Query Separation
  • 모든 메서드는 명령이거나 조회여야 함
구분 Command (명령) Query (조회)
의미 상태 변경 연산
상태 조회 연산
부작용
반환값 (void)
테스트 대역 Mock Stub
검증 포인트
“이 메서드가 외부에 잘 전달되었는가?”
“이 메서드가 기대한 데이터를 반환하는가?”
예시
사용자 등록, 이메일 전송, 로그 기록 
사용자 조회, 설정 값 읽기, 상태 확인

 

예시) Command

더보기
public class UserService {
    private final EmailService emailService;

    public UserService(EmailService emailService) {
        this.emailService = emailService;
    }

    public void registerUser(User user) {
        // 유저 등록 로직 (예: DB 저장)
        // ...

        // 이메일 전송 (부작용)
        emailService.sendWelcomeEmail(user.getEmail());
    }
}
@Test
void registerUser_sendsWelcomeEmail() {
    EmailService emailService = mock(EmailService.class);
    UserService userService = new UserService(emailService);
    User user = new User("noah@example.com");

    userService.registerUser(user);

    // sendWelcomeEmail 호출 여부 검증
    verify(emailService).sendWelcomeEmail("noah@example.com");
}

 

예시) Query

더보기
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUser(String email) {
        return userRepository.findByEmail(email);  // 조회 (부작용 없음)
    }
}
@Test
void getUser_returnsUser() {
    UserRepository userRepository = mock(UserRepository.class);
    UserService userService = new UserService(userRepository);
    User expectedUser = new User("noah@example.com");

    when(userRepository.findByEmail("noah@example.com")).thenReturn(expectedUser);

    User result = userService.getUser("noah@example.com");

    assertEquals(expectedUser, result);
}

 

2. 식별할 수 있는 동작과 구현 세부 사항

식별할 수 있는 동작 vs 구현 세부 사항

구분 식별할 수 있는 동작 구현 세부 사항
클라이언트 관점 무엇을 할 수 있는가?
어떻게 작동하는가?
API 목적 사용자의 목표 달성 내부 작동 방식
위험 요소 캡슐화 깨짐

 

식별할 수 있는 동작 ≠ 공개 API

  • 공개 API: 외부에서 쉽게 사용할 수 있도록 설계 (무조건 의미 있는 동작을 제공하지는 않음)
  • 비공개 API: 외부에서 요청만으로 처리하도록 설계 (내부에서 책임지고 처리하는 구조)

 

구현 세부 사항 유출

구분 설명 문제점
연산 (메서드 조합) 여러 개의 메서드를 호출해야 하는 경우
캡슐화 깨짐
- 행위와 역할 분리 (클라이언트가 상태를 보고 직접 처리해야 함)

불변성 위반 가능
- 여러 연산이 복잡하게 얽혀 상태 변경이 명확히 드러나지 않음
상태 (데이터 노출) 상태 값을 외부에 노출하는 경우
캡슐화 깨짐
- 행위와 역할 분리 (클라이언트가 상태를 보고 직접 처리해야 함)

시스템 일관성 깨질 위험
- 내부 로직이 외부에 의존
- 외부 코드 변경 시, 예상했던 동작과 다르게 동작함

 

 예시) 연산 유출

더보기

// Client가 여러 메서드를 조합해야 한다 (불변성 깨질 수 있음)
User user = new User();
user.validateEmail(email);
user.saveToDatabase();
user.sendWelcomeEmail();

 

// 내부적으로 순서와 불변성을 관리
UserService userService = new UserService();
userService.registerUser(email);

 

예시) 상태 유출

더보기

// balance를 노출하고 클라이언트가 판단
Account account = accountRepository.findById(id);
if (account.getBalance() >= amount) {
    account.withdraw(amount);
}

 

// 의미 있는 행동만 제공
Account account = accountRepository.findById(id);
if (account.canWithdraw(amount)) {
    account.withdraw(amount);
}