블라디미르 코리코프 님의 "단위 테스트" 책을 정리한 포스팅입니다.
1. 좋은 단위 테스트의 4대 요소
요소 | 설명 | 문제 상황 / 원인 | 해결 방안 |
회귀 방지 | 기존 기능이 수정으로 인해 망가지지 않도록 방지 | 코드 변경 후 기능이 동작하지 않음 |
테스트를 통한 기능 보호
책임 있는 코드 작성 |
리팩터링 내성 | 리팩터링 시에도 테스트가 정상적으로 동작하는 성질 |
구현에 밀접하게 의존
상태 검증이 불안정 |
결과 기반 테스트
구현 디테일 분리 |
└ 거짓 양성 (허위 경보) |
실제로는 문제 없지만 테스트는 실패하는 상황 | ||
빠른 피드백 | 테스트가 빠르게 실행되어야 실시간 피드백이 가능 | 외부 의존성으로 느려지거나 무거워짐 |
빠른 단위 테스트 설계
외부 의존성 격리 |
유지 보수성 |
가독성, 유지 비용, 실행 비용 등을 평가
|
||
└ 이해 난이도 | 테스트가 얼마나 직관적이고 쉽게 읽히는가 | 복잡한 준비 코드 암시적 의미 |
AAA 패턴 사용
설명적인 테스트 이름 |
└ 실행 난이도 | 테스트가 얼마나 빠르고 독립적으로 실행되는가 | 외부 리소스 의존성 |
의존성 분리
Stub/Fake 등 사용 |
예시) 회귀 방지
더보기
❌
// 누군가 실수로 곱셈으로 바꿨음
public class Calculator {
public int add(int a, int b) {
return a * b; // ❌ 버그
}
}
// 테스트가 없거나 add에 대한 검증이 빠짐
// 이 상태에선 테스트가 실패하지 않고 버그가 감지되지 않음 ❌
✅
// 기존 기능
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
// 테스트 (회귀 방지)
public class CalculatorTest {
@Test
public void testAdd_twoPositiveNumbers_returnsCorrectSum() {
Calculator calculator = new Calculator();
assertEquals(5, calculator.add(2, 3)); // ✅ 회귀 방지
}
}
- 이후 add() 로직이 바뀌더라도 이 테스트가 보장해줌.
예시) 리팩터링 내성
더보기
public class UserFormatter {
public String formatUser(User user) {
return user.getFirstName() + " " + user.getLastName();
}
}
❌ (테스트에서 내부 구현을 지나치게 많이 참조하는 경우)
// 포맷 로직을 캡슐화하거나 외부 위임하게 됨
public String formatUser(User user) {
return user.getFullName(); // 내부 호출 방식 변경
}
@Test
public void testFormatUser_usesFirstNameAndLastNameDirectly() {
UserFormatter formatter = new UserFormatter();
User mockUser = mock(User.class);
when(mockUser.getFirstName()).thenReturn("John");
when(mockUser.getLastName()).thenReturn("Doe");
String result = formatter.formatUser(mockUser);
verify(mockUser).getFirstName(); // ❌ 내부 호출을 검증
verify(mockUser).getLastName(); // ❌
assertEquals("John Doe", result);
}
- 리팩토링 시 내부 구현이 바뀌면 깨짐
- 구현 세부사항에 의존한 탓
✅ (결과만 검증)
@Test
public void testFormatUser_returnsCorrectFullName() {
User user = new User("John", "Doe");
UserFormatter formatter = new UserFormatter();
assertEquals("John Doe", formatter.formatUser(user)); // ✅ 외부 동작만 검증
}
- 구현 변경이 있어도 테스트가 깨지지 않음
예시) 빠른 피드백
더보기
// 외부 API 대신 Fake 객체 사용
public interface PaymentGateway {
boolean charge(int amount);
}
public class FakePaymentGateway implements PaymentGateway {
@Override
public boolean charge(int amount) {
return true; // 실제 결제 아님
}
}
public class OrderService {
private final PaymentGateway gateway;
public OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
public boolean placeOrder(int amount) {
return gateway.charge(amount);
}
}
// 테스트 (빠르고 독립적)
public class OrderServiceTest {
@Test
public void testPlaceOrder_chargesPaymentSuccessfully() {
OrderService service = new OrderService(new FakePaymentGateway());
assertTrue(service.placeOrder(100)); // ✅ 빠른 피드백
}
}
- 외부 의존성 제거하고 빠르게 동작하도록 구성
예시) 유지 보수성
더보기
public class DiscountCalculator {
public int applyDiscount(int price, int rate) {
return price - (price * rate / 100);
}
}
// 명확한 테스트 구조 + SUT/의존성 분리
public class DiscountCalculatorTest {
@Test
public void testApplyDiscount_appliesCorrectRate() {
// Arrange
DiscountCalculator calculator = new DiscountCalculator();
// Act
int discounted = calculator.applyDiscount(1000, 10);
// Assert
assertEquals(900, discounted); // ✅ 유지 보수성 높음
}
}
- 간결하고 읽기 쉬운 테스트 구조 + 명확한 이름 + 적절한 캡슐화
2. 회귀 방지와 리팩터링 내성
개념 | 설명 | 관련성 |
거짓 음성 (False Negative) |
테스트가 기능 고장을 잡아내지 못하는 경우 (기능은 고장났으나 테스트는 이를 발견하지 못함) |
회귀 방지가 거짓 음성을 방지하는 데 도움
|
거짓 양성 (False Positive) |
테스트가 기능 고장이 아닌 경우를 고장으로 잘못 판단하는 경우 (기능은 정상인데 테스트가 실패하는 경우) |
리팩터링 내성이 거짓 양성을 줄이는 데 도움
|
테스트 정확도 향상
- 회귀 방지와 리팩터링 내성이 모두 테스트 정확도를 높인다.
- 신호를 증가시켜 버그를 찾아내고, 소음을 줄여 허위 경보를 줄인다.
'Code > Test' 카테고리의 다른 글
[단위 테스트] 5-1. 목과 테스트 취약성: 목 (0) | 2025.01.24 |
---|---|
[단위 테스트] 4-2. 좋은 단위 테스트의 4대 요소: 이상적인 테스트 (0) | 2025.01.24 |
[단위 테스트] 3. 단위 테스트 구조 (0) | 2025.01.07 |
[단위 테스트] 2-2. 단위 테스트란 무엇인가: 런던파와 고전파 (1) | 2025.01.07 |
[단위 테스트] 2-1. 단위 테스트란 무엇인가: 단위 테스트란 (0) | 2025.01.07 |