블라디미르 코리코프 님의 "단위 테스트" 책을 정리한 포스팅입니다.
1. AAA 패턴
- 테스트를 준비, 실행, 검증 세 부분으로 나누어 구성하는 방법
- 스위트 내 모든 테스트가 단순하고 균일한 구조를 갖는 데 도움이 됨
단계 | 설명 |
Arrange |
테스트를 위한 객체(SUT) 및 의존성 설정
|
Act |
테스트 대상 메서드 호출 (동작 실행)
|
Assert |
결과 확인 (반환 값, 상태 변화, 호출 여부 등 검증)
|
2. 권장 사항
항목 | 권장 사항 | 이유 및 설명 |
구절 수 제한 | 한 테스트 내 여러 Arrange, Act, Assert 구절 피하기 | 단일 동작 단위만 검증해야 함 여러 실행은 단위 테스트가 아닌 통합 테스트임 |
조건문 회피 | 테스트 내 if, switch 등 분기문 피하기 |
복잡도 증가
테스트 이해도 저하 한 테스트에 여러 경우를 넣지 말고 분리해야 함 |
메서드 캡슐화 | 구절이 길어질 경우, private 메서드로 분리 |
반복 코드 제거
가독성 향상 테스트 목적 파악이 쉬워짐 |
불변 위반 방지 | 여러 호출로 한 결과를 만드는 구조 피하기 |
불변성 깨질 수 있음
한 동작 = 한 결과로 캡슐화 필요 |
적정 검증 수 | assert는 많을 수 있지만, 동작 단위는 하나여야 |
한 테스트 = 한 동작 단위 원칙 유지
|
동등 멤버 정의 | 복잡한 검증은 equals() 또는 Comparable 정의 |
assertEquals 등에서 객체 상태 비교를 단순화할 수 있음
|
종료 구문 | 단위 테스트에서는 tearDown() 거의 불필요 |
대부분 자원 정리 불필요
통합 테스트나 자원 할당 테스트일 때만 사용 |
SUT 구분 명확히 | 테스트 대상(SUT)과 의존성 구분해서 선언 |
어떤 객체가 테스트 대상인지 직관적으로 파악 가능해야 함
|
예제) 구절 수 제한
더보기
❌
@Test
void testMultipleActionsInOneTest() {
OrderService service = new OrderService();
// Act & Assert 1
String result1 = service.order("item1");
assertEquals("Order item1", result1);
// Act & Assert 2
String result2 = service.order("item2");
assertEquals("Order item2", result2);
}
✅
@Test
void testOrderItem1() {
OrderService service = new OrderService();
String result = service.order("item1");
assertEquals("Order item1", result);
}
@Test
void testOrderItem2() {
OrderService service = new OrderService();
String result = service.order("item2");
assertEquals("Order item2", result);
}
예제) 조건문 피하기
더보기
❌
@Test
void testOrderConditional() {
OrderService service = new OrderService();
String item = "item1";
if (item.equals("item1")) {
assertEquals("Order item1", service.order(item));
} else {
assertEquals("Order item2", service.order(item));
}
}
✅
@Test
void testOrderItem1() {
OrderService service = new OrderService();
assertEquals("Order item1", service.order("item1"));
}
예제) 메서드 비공개 캡슐화
더보기
✅
@Test
void testOrder() {
OrderService service = new OrderService();
String result = service.order("item1");
assertOrderResult(result, "Order item1");
}
private void assertOrderResult(String actual, String expected) {
assertEquals(expected, actual);
}
예제) 불변 위반 방지
더보기
❌
@Test
void testPriceCalculation() {
OrderService service = new OrderService();
service.addItem("item1", 10);
service.addItem("item2", 20);
service.addItem("item3", 30);
int total = service.calculateTotal();
assertEquals(60, total);
}
✅
@Test
void testCalculateTotalWithSingleCall() {
OrderService service = new OrderService();
service.addItems(List.of("item1", "item2", "item3"), List.of(10, 20, 30));
int total = service.calculateTotal();
assertEquals(60, total);
}
예제) 적정 검증 수
더보기
✅
@Test
void testOrderContainsInfo() {
OrderService service = new OrderService();
String result = service.order("item1");
assertTrue(result.contains("Order"));
assertTrue(result.contains("item1"));
}
- 하나의 동작을 다양한 측면에서 검증할 때는 괜찮음.
예제) 동등 멤버 정의
더보기
✅
@Test
void testUserEquality() {
User expected = new User("noah", 30);
User actual = userRepository.findByName("noah");
assertEquals(expected, actual); // equals() 오버라이드 되어 있어야 함
}
예제) 종료 구문
더보기
✅
@Test
void testSimple() {
OrderService service = new OrderService();
assertEquals("Order item1", service.order("item1"));
}
- 단위 테스트에선 생략
예제) SUT 구분 명확히
더보기
❌
@Test
void testSomething() {
new OrderService(new FakeRepo()).order("item1"); // 뭐가 SUT인지 불명확
}
✅
@Test
void testOrder() {
OrderRepository fakeRepo = new FakeOrderRepository();
OrderService sut = new OrderService(fakeRepo);
String result = sut.order("item1");
assertEquals("Order item1", result);
}
3. 테스트 간 픽스처 재사용
- 테스트 실행 대상 객체 관련해서, 테스트 간 공통으로 사용되는 부분을 별도의 메서드나 클래스로 도출하여 재사용
- 의존성, 셋팅 등
장점
항목 | 설명 |
코드량 감소 |
여러 테스트에서 공통 객체(픽스처)를 재사용하여 테스트 코드 양을 줄일 수 있음.
|
중복 제거 |
유사한 객체를 반복적으로 생성하지 않아도 되어 깔끔한 코드 작성 가능.
|
예제) 장점
더보기
❌
class OrderServiceTest {
@Test
void testOrderTotalPrice() {
OrderItem item1 = new OrderItem("Book", 2, 10000);
OrderItem item2 = new OrderItem("Pen", 3, 2000);
Order order = new Order(List.of(item1, item2));
int total = order.calculateTotalPrice();
assertEquals(10000 * 2 + 2000 * 3, total);
}
@Test
void testOrderContainsItem() {
OrderItem item1 = new OrderItem("Book", 2, 10000);
OrderItem item2 = new OrderItem("Pen", 3, 2000);
Order order = new Order(List.of(item1, item2));
assertTrue(order.containsItem("Pen"));
}
}
✅
class OrderServiceTest {
Order order;
@BeforeEach
void setUp() {
OrderItem item1 = new OrderItem("Book", 2, 10000);
OrderItem item2 = new OrderItem("Pen", 3, 2000);
order = new Order(List.of(item1, item2));
}
@Test
void testOrderTotalPrice() {
int total = order.calculateTotalPrice();
assertEquals(10000 * 2 + 2000 * 3, total);
}
@Test
void testOrderContainsItem() {
assertTrue(order.containsItem("Pen"));
}
}
단점
항목 | 설명 |
높은 결합도 |
픽스처를 공유할 경우 테스트 간 의존성이 생겨 하나의 테스트가 다른 테스트에 영향을 줄 수 있음.
|
공유 상태 위험 |
객체의 상태가 변경될 수 있다면 공유 픽스처가 여러 테스트에 영향을 미칠 수 있음.
|
디버깅 어려움 |
테스트 실패 시 원인을 찾기 어려워지고, 한 테스트 수정이 다른 테스트 실패를 유발할 수 있음.
|
가독성 저하 |
생성자 또는 설정 메서드에서 픽스처가 구성되면, 실제 테스트가 무엇을 검증하는지 바로 보기 어려움.
|
테스트 코드 이해 어려움 |
픽스처의 정의와 사용 위치가 분리되면 테스트 목적을 파악하기 어려워짐.
|
예제) 단점
더보기
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. 단위 테스트 명명법
- 도메인 언어 기반
- 엄격한 규칙보다는 가독성과 명확성이 중요 (메서드명_상황_기대결과)
- “이런 조건에서 이렇게 동작해야 한다”는 시나리오 기반이 바람직함 (Given/When/Then 형식)
예시) 도메인 언어 기반
더보기
@Test
void 직원이_근무시간을_초과하면_초과수당이_계산된다() {
Payroll payroll = new Payroll();
int result = payroll.calculateOvertime(50); // 기준: 40시간
assertEquals(10 * OVERTIME_RATE, result);
}
예시) 시나리오 기반
더보기
@Test
void add_WhenTwoPositiveNumbers_ReturnsTheirSum() {
assertEquals(5, calculator.add(2, 3));
}
@Test
void login_WithCorrectCredentials_ReturnsTrue() {
assertTrue(authService.login("user", "pass"));
}
@Test
void login_WithInvalidPassword_ReturnsFalse() {
assertFalse(authService.login("user", "wrongpass"));
}
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 > Test' 카테고리의 다른 글
[단위 테스트] 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 |