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);
}