블라디미르 코리코프 님의 "단위 테스트" 책을 정리한 포스팅입니다.
1. Mock vs Stub
- 테스트 대상 시스템과 그 협력자 사이의 상호 작용을 검사할 수 있는 테스트 대역
구분 | Mock (목) | Stub (스텁) |
역할 | 외부로 나가는 상호 작용을 모방하고 검증 |
내부로 들어오는 데이터/행위를 모방
|
목적 | SUT가 의존 대상에 정확히 호출했는지 검증 |
SUT가 의존 대상에서 입력 데이터를 제공받도록 구성
|
방향 | Out-bound (외부로 나가는 호출) |
In-bound (내부로 들어오는 호출)
|
검증 여부 | ✅ 상호작용 검증 (호출 횟수, 순서, 인자 등) |
❌ 호출 여부나 인자 등 검증하지 않음
|
예시 | 이메일 전송 여부, 결제 API 호출 여부 등 |
DB에서 회원 조회 결과 반환, 외부 API에서 응답 반환 등
|
적합한 상황 | 부작용 발생 확인 (ex. 알림, 로그 저장, 외부 API) |
데이터 주입 (ex. 사용자 정보, 설정 값, 통신 결과)
|
주의사항 | 과도한 상호작용 검증은 테스트 유지보수성을 해칠 수 있음 |
스텁에 대한 상호작용 검증은 하지 말 것
|
테스트 위험 | 과도한 목 검증 → 과잉 명세 |
스텁 호출 검증 시 → 구현 세부사항 노출로 테스트 취약
|
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. 식별할 수 있는 동작과 구현 세부 사항
식별할 수 있는 동작은 공개 API와 다르다
- 공개 API는 식별할 수 있는 동작을 제공하여, 외부에서 쉽게 사용할 수 있도록 설계되어야 함
- 비공개 API는 외부와의 상호작용에서 내부 동작을 독립적으로 처리할 수 있도록 설계되어야 함
식별할 수 있는 동작 vs 구현 세부 사항
구분 | 식별할 수 있는 동작 | 구현 세부 사항 |
클라이언트 관점 | 무엇을 할 수 있는가? |
어떻게 작동하는가?
|
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);
}
'Code > Test' 카테고리의 다른 글
[단위 테스트] 6-1. 단위 테스트 스타일: 단위 테스트 스타일 (0) | 2025.01.25 |
---|---|
[단위 테스트] 5-2. 목과 테스트 취약성: 통신 (0) | 2025.01.24 |
[단위 테스트] 4-2. 좋은 단위 테스트의 4대 요소: 이상적인 테스트 (0) | 2025.01.24 |
[단위 테스트] 4-1. 좋은 단위 테스트의 4대 요소: 4대 요소 (0) | 2025.01.19 |
[단위 테스트] 3. 단위 테스트 구조 (0) | 2025.01.07 |