테스트 환경을 위해서 원하는 상태 값으로 고정되어 있는 객체를 말한다. 테스트 환경을 설정하는데 사용되며, 테스트 코드의 가독성과 유지보수성을 높이기 위해 사용된다.
Setup & Teardown
테스트가 시작되기 전과 완료된 후 테스트 환경을 설정하거나 해제하는 작업을 말하며, 테스트 시작이나 종료 혹은 특정 시점에 설정 작업을 수행할 수 있다.
publicclassOrderProcessingServiceTest {privateOrderProcessingService orderProcessingService;privateInventoryService inventoryService;privatePaymentService paymentService; @BeforeEachvoidsetUp() {// 공통적으로 필요한 서비스 초기화 inventoryService =newInventoryService(); paymentService =newPaymentService();// OrderProcessingService 초기화 orderProcessingService =newOrderProcessingService(inventoryService, paymentService); } @AfterEachvoidtearDown() {// 테스트 종료 후 리소스 해제inventoryService.clear(); } @Test @DisplayName("재고가 충분하고 결제가 성공하면 주문이 성공한다.")voidtestOrderSuccess() {// Given: 필요한 아이템을 재고에 추가Item item1 =newItem("item1",100.0);inventoryService.addItem(item1,10);// When: 아이템을 주문할 때Order order =newOrder(item1,2);boolean result =orderProcessingService.processOrder(order);// Then: 주문이 성공해야 한다assertTrue(result);assertEquals(8,inventoryService.getStock(item1)); } @Test @DisplayName("재고가 부족한 경우 주문이 실패해야 한다.")voidtestOrderFailureDueToInsufficientStock() {// Given: 재고를 낮게 설정Item item2 =newItem("item2",50.0);inventoryService.addItem(item2,1);// When: 재고보다 많은 수량을 주문할 때Order order =newOrder(item2,2);boolean result =orderProcessingService.processOrder(order);// Then: 주문이 실패해야 한다assertFalse(result);assertEquals(1,inventoryService.getStock(item2)); // 재고는 그대로 남아야 한다 }}
특정 객체를 공유하거나 반복해서 객체를 생성하는 코드가 반복되는 경우 사용할 수 있는데, 이를 통해 테스트 코드의 중복을 줄이고 가독성을 높일 수 있다.
테스트 독립성
설정하는 순간 모든 테스트에 공통으로 영향을 줄 수 있으므로, 아래의 조건을 만족하는 경우에만 추가하는 것을 고려하는 것이 좋다.
각 테스트 입장에서 봤을 때 테스트 내용을 이해하는데 문제가 없음
수정해도 모든 테스트에 영향을 주지 않음
만약 위 코드의 @BeforeEach에서 기본 아이템을 추가하는 로직을 넣었다면, 주문 실패 로직에 영향을 끼쳐 테스트가 실패할 수 있다.
@BeforeEachvoidsetUp() {// 공통적으로 필요한 서비스 초기화 inventoryService =newInventoryService(); paymentService =newPaymentService();// !!모든 테스트에 영향을 주는 로직inventoryService.addItem(newItem("item1",100.0),10);inventoryService.addItem(newItem("item2",50.0),1);// OrderProcessingService 초기화 orderProcessingService =newOrderProcessingService(inventoryService, paymentService);}
Test Data 생성 로직 분리
테스트 코드에서 사용하는 데이터를 생성하는데 사용되며, 하나의 도메인 인스턴스가 여러 테스트에서 사용되는 경우 테스트 코드의 중복을 줄이고 가독성을 높이기 위해 사용된다.
publicclassOrderProcessingServiceTest {// ...// Item 객체를 생성하는 메서드privateItemcreateDefaultItem(String name,double price) {returnItem.builder().name(name).price(price).build(); } @Test @DisplayName("재고가 충분하고 결제가 성공하면 주문이 성공한다.")voidtestOrderSuccess() {// Given: 필요한 아이템을 재고에 추가Item item1 =createDefaultItem("item1",100.0);inventoryService.addItem(item1,10);// When: 아이템을 주문할 때Order order =newOrder(item1,2);boolean result =orderProcessingService.processOrder(order);// Then: 주문이 성공해야 한다assertTrue(result);assertEquals(8,inventoryService.getStock(item1)); }// ...}
Test Data Factory
테스트 데이터를 생성하는 클래스를 따로 만들어서 사용할 수 있는데, 그에 대한 장단점은 다음과 같다.
장점
테스트 데이터 생성 로직을 중복해서 작성하지 않아도 됨
테스트 데이터 생성 로직을 한 곳에서 관리 가능
단점
필요한 인자 갯수가 많고 한 클래스에서 다양한 종류의 데이터를 생성하는 경우 복잡해질 수 있음
publicclassTestDataFactory {publicstaticItemcreateDefaultItem(String name,double price) {returnItem.builder().name(name).price(price).build(); }// 유사한 메서드가 늘어나면 관리가 어려워질 수 있음publicstaticItemcreateItemWithQuantity(String name,double price,int quantity) {returnItem.builder().name(name).price(price).quantity(quantity).build(); }}
Parameterized Test
하나의 입력 값만으론 충분한 테스트가 불충분하다고 판단되는 경우, 여러 입력 값에 대해 반복적으로 테스트를 수행하는 방법이다.
importstaticorg.junit.jupiter.api.Assertions.assertEquals;importorg.junit.jupiter.params.ParameterizedTest;importorg.junit.jupiter.params.provider.CsvSource;publicclassOrderServiceTest {// ...// 1. CsvSource를 사용한 Parameterized Test @ParameterizedTest(name ="{index} => 상품: {0}, 가격: {1}, 수량: {2} => 총 가격: {3}") @CsvSource({"item1, 100.0, 2, 200.0","item2, 50.0, 3, 150.0","item3, 200.0, 1, 200.0" }) @DisplayName("각 상품의 총 가격이 올바르게 계산된다.")voidtestCalculateTotalPrice(String itemName,double price,int quantity,double expectedTotal ) {// Given: 아이템 생성Item item =newItem(itemName, price);Order order =newOrder(item, quantity);// When: 주문을 처리하고 총 가격을 계산double totalPrice =orderService.calculateTotalPrice(order);// Then: 총 가격이 예상과 일치해야 한다assertEquals(expectedTotal, totalPrice); }// ========================================================================// 2. MethodSource를 사용한 Parameterized Test// 테스트 데이터를 제공하는 메서드staticStream<Arguments> provideOrders() {returnStream.of(Arguments.of(newItem("item1",100.0),2,200.0),Arguments.of(newItem("item2",50.0),3,150.0),Arguments.of(newItem("item3",200.0),1,200.0) ); } @ParameterizedTest(name ="{index} => 상품: {0}, 수량: {1} => 총 가격: {2}") @MethodSource("provideOrders") @DisplayName("각 상품의 총 가격이 올바르게 계산된다.")voidtestCalculateTotalPrice(Item item,int quantity,double expectedTotal) {// Given: Order 생성Order order =newOrder(item, quantity);// When: 주문을 처리하고 총 가격을 계산double totalPrice =orderService.calculateTotalPrice(order);// Then: 총 가격이 예상과 일치해야 한다assertEquals(expectedTotal, totalPrice); }}
대표적으로 @CsvSource와 @MethodSource를 사용하는 두 가지 방식이 있으며, 장단점과 적합한 상황에 따라 선택하여 사용하면 된다.