블라디미르 코리코프 님의 "단위 테스트" 책을 정리한 포스팅입니다.
1. AAA 패턴
- 테스트를 준비, 실행, 검증 세 부분으로 나누어 구성하는 방법
구조
- 준비: SUT와 의존성 셋팅
- 실행: SUT에서 메서드 호출 및 의존성 전달
- 검증: 결과 검증. (반환 값이나 SUT나 협력자의 상태, SUT가 호출한 메서드 등)
장점
- 일관성: 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 됨
2. 권장 사항
여러 개의 준비, 실행, 검증 구절 피하기
- 여러 개의 실행 구절은 여러 개의 동작 단위를 검증하는 테스트임을 의미함
- 이러한 구조는 단위 테스트가 아닌 통합 테스트임을 의미함
테스트 내 if 문 피하기
- 한번에 너무 많은 것을 검증한다는 표시
- 분기가 있어서 얻는 이점이 없음
- 테스트를 읽고 이해하는 것을 더 어렵게 함
- 반드시 여러 테스트로 나눠야 함
구절이 길어지면, 별도의 비공개 메서드로 캡슐화하기
- 불변 위반 주의하기
- 한 결과를 위해 여러 메서드의 호출이 필요하다면 모순이 발생할 수 있음
- 한 메서드의 호출로 결과가 도출되도록 캡슐화 필요
검증 구절에는 검증문이 얼마나 있어야 하는가
- 검증 단위는 코드가 아닌 동작의 단위
- 그러나 검증문이 너무 커진다면 좋지 않음
- 동등 멤버를 정의하여 처리하는 방식이 권장됨
종료 단계는 어떤가
- 테스트에 사용된 자원을 정리하는 단계
- 단위 테스트에선 사용할 일 없음 (통합 테스트의 단계)
테스트 대상 시스템 구별하기
- SUT는 애플리케이션에서 호출하고자 하는 동작에 대한 진입점을 제공함
- SUT와 의존성을 구분하여 직관적으로 테스트문을 읽을 수 있도록 하기
3. 테스트 간 픽스처 재사용
- 테스트 실행 대상 객체 관련해서, 테스트 간 코드를 재사용 시 사용되는 부분 (의존성, 셋팅 등)
- 별도의 메서드나 클래스로 도출
장점
- 테스트 코드 양을 크게 줄일 수 있음
단점
class OrderServiceTest {
private static DatabaseConnection sharedConnection; // 공유 상태 (안티패턴)
private OrderService orderService;
@BeforeAll
static void setupDatabase() {
// 테스트 간 공유되는 DB 연결 (안티패턴)
sharedConnection = new DatabaseConnection("test-db-url");
}
@BeforeEach
void setup() {
// 생성자를 통한 의존성 설정 (안티패턴)
orderService = new OrderService(new OrderRepository(sharedConnection));
}
@Test
void testPlaceOrder() {
Order order = orderService.placeOrder("item1", 2);
assertNotNull(order.getId());
}
@Test
void testCancelOrder() {
Order order = orderService.placeOrder("item1", 2);
boolean success = orderService.cancelOrder(order.getId());
assertTrue(success);
}
}
테스트 간의 높은 결합도는 안티 패턴이다
- 공유 상태를 가질 경우 모든 테스트가 서로 결합됨
- 테스트를 수정해도 다른 테스트에 영향을 주어선 안됨
- 즉, 공유 상태를 두어선 안됨
테스트 가독성을 떨어뜨리는 생성자 사용
- 테스트 코드가 무엇을 검증하는지 보려면 다른 부분도 봐야 함
해결책
class OrderServiceTest {
private OrderService orderService;
@BeforeEach
void setup() {
// 비공개 팩토리 메서드를 통해 OrderService 생성
orderService = createOrderService();
}
private OrderService createOrderService() {
OrderRepository orderRepository = createDependency(OrderRepository.class);
return new OrderService(orderRepository);
}
private <T> T createDependency(Class<T> type) {
if (type == DatabaseConnection.class) {
return (T) new InMemoryDatabaseConnection(); // 독립적 테스트 환경
} else if (type == OrderRepository.class) {
DatabaseConnection connection = createDependency(DatabaseConnection.class);
return (T) new OrderRepository(connection);
}
throw new IllegalArgumentException("Unsupported dependency type: " + type.getName());
}
@Test
void testPlaceOrder() {
Order order = orderService.placeOrder("item1", 2);
assertNotNull(order.getId());
}
@Test
void testCancelOrder() {
Order order = orderService.placeOrder("item1", 2);
boolean success = orderService.cancelOrder(order.getId());
assertTrue(success);
}
}
비공개 팩토리 메서드 두기
- 테스트 코드르 짧게 하기
- 테스트 진행 상황에 대한 전체 맥락을 유지함
4. 단위 테스트 명명법
- 엄격한 명명 X
- 문제 도메인에 익숙한 비개발자에게 시나리오를 설명하듯
5. 매개변수화된 테스트 리팩터링하기
- 테스트 하나로 동작 단위를 완벽하게 설명하기 힘든 경우가 많음
- 여러 구성 요소를 포함하며, 각 구성 요소는 자체 테스트로 캡처해야 함
- 입력과 예상 결과만 다른데도 분리됨
매개변수화된 테스트
class NumberUtilsTest {
@ParameterizedTest
@CsvSource({
"2, true", // 짝수
"3, false", // 홀수
"0, true", // 특수 케이스 (0은 짝수로 간주)
"-4, true", // 음수 짝수
"-5, false" // 음수 홀수
})
void testIsEven(int number, boolean expected) {
assertEquals(expected, NumberUtils.isEven(number));
}
}
- 유사한 테스트를 묶는 기능
- 테스트 코드 양이 줄어들음
- 단, 긍정적이거나 중요한 테스트는 독립적으로 도출해서 명명하는 것이 좋음
'Code' 카테고리의 다른 글
[단위 테스트] 4-2. 좋은 단위 테스트의 4대 요소: 이상적인 테스트 (0) | 2025.01.24 |
---|---|
[단위 테스트] 4-1. 좋은 단위 테스트의 4대 요소: 4대 요소 (0) | 2025.01.19 |
[단위 테스트] 2-2. 단위 테스트란 무엇인가: 런던파와 고전파 (1) | 2025.01.07 |
[단위 테스트] 2-1. 단위 테스트란 무엇인가: 단위 테스트란 (0) | 2025.01.07 |
[단위 테스트] 1-2. 단위 테스트의 목표: 커버리지 지표 (1) | 2025.01.06 |