테스트 코드의 가독성 향상: BDD 방법론 @DisplayName의 효과적 활용
서론
현대 소프트웨어 개발에서 테스트 코드는 단순히 기능의 정확성을 검증하는 것을 넘어, 코드의 명확한 문서화와 커뮤니케이션 도구로서 중요한 역할을 담당한다. 테스트 코드의 가독성을 향상시키는 데에는 여러 전략이 있지만, 이번 글에서는 우리 프로젝트에서 적용중인 BDD(Behavior-Driven Development)와 @DisplayName 어노테이션의 활용을 중점으로 설명하겠다.
BDD란 무엇인가?
BDD(Behavior-Driven Development)는 소프트웨어 개발 과정에서 행동(behavior)을 중심으로 하는 접근 방법이다. 이 방식은 Dan North에 의해 개발되었으며, TDD(Test-Driven Development)의 원칙을 확장하여 비즈니스 요구사항과 기술 사양을 명확하게 연결한다. BDD는 소프트웨어 개발과 테스트가 사용자의 행동과 상호작용에 기반하여 이루어져야 한다는 관점을 제공한다.
BDD와 TDD의 관계
TDD에서는 테스트를 먼저 작성하고, 그 테스트를 통과하는 코드를 개발한다. BDD는 TDD의 기본 원칙을 따르면서, '시나리오'를 통해 비즈니스 요구사항을 더 명확하게 표현한다.
BDD의 핵심은 비즈니스 요구사항을 자연 언어 스타일의 시나리오로 전환하여, 비개발자도 이해할 수 있는 형태로 테스트를 작성하는 것이다. 이 방식은 비즈니스 요구사항을 코드에 더 밀접하게 연결하고, 개발자와 비개발자 간의 커뮤니케이션을 개선한다.
BDD의 실제 적용 방법 - 서비스 클래스 (단위 테스트 전략 적용)
BDD에서는 'Given-When-Then' 패턴을 사용하여 테스트 케이스를 구성한다:
- Given (준비 단계): 테스트를 위한 초기 상태를 설정한다. 여기에는 필요한 데이터나 조건을 구성하는 작업이 포함된다.
- When (실행 단계): 테스트의 핵심 동작이나 메소드를 실행한다. 여기서는 검증하고자 하는 기능이 수행된다.
- Then (검증 단계): 실행 단계의 결과를 검증한다. 기대하는 결과가 실제와 일치하는지 확인하는 단계이다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.InjectMocks;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat;
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@InjectMocks
private OrderService sut;
@DisplayName("[happy] 주문 상태 업데이트 성공")
@Test
void updateOrderStatusTestSuccess() {
// Given
Long orderId = 1L;
OrderStatus newStatus = OrderStatus.DELIVERED;
Order order = new Order(orderId, OrderStatus.CREATED);
when(orderRepository.findById(orderId)).thenReturn(Optional.of(order));
// When
sut.updateOrderStatus(orderId, newStatus);
// Then
assertThat(order.getStatus()).isEqualTo(OrderStatus.DELIVERED);
}
@DisplayName("[fail] 주문 상태 업데이트 실패 - 존재하지 않는 주문")
@Test
void updateOrderStatusTestFail() {
// Given
Long orderId = 99L; // 존재하지 않는 주문 ID
OrderStatus newStatus = OrderStatus.DELIVERED;
when(orderRepository.findById(orderId)).thenReturn(Optional.empty());
// When
Exception exception = assertThrows(OrderNotFoundException.class, () -> {
sut.updateOrderStatus(orderId, newStatus);
});
// Then
assertThat(exception.getMessage()).contains("Order not found");
}
}
위 코드의 given, when, then을 설명해보겠다.
Given (준비 단계)
테스트를 위한 초기 상태를 설정한다. 여기서는 테스트할 주문(Order) 객체를 생성하고, orderRepository의 findById 메소드가 특정 주문 ID에 대해 해당 주문 객체를 반환하도록 설정한다.
When (실행 단계)
테스트의 핵심 행동을 실행한다. sut.updateOrderStatus 메소드를 호출하여 주문 상태를 업데이트한다.
Then (검증 단계)
sut에 의해 실행된 행동의 결과를 검증한다. 주문 상태가 업데이트되어 'DELIVERED'로 변경되었는지 확인한다.
sut는 "System Under Test"의 약어로, 주로 테스트 코드에서 사용되는 용어다. 이 변수명은 주로 테스트 대상인 클래스나 메서드를 가리키는데 사용된다. sut 변수는 OrderService 클래스의 인스턴스를 가리키고 있으며, 해당 클래스가 테스트 대상이라는 것을 나타낸다.
테스트 코드에서 sut를 사용하는 이유는 주로 다음과 같다:
테스트 대상을 명확하게 식별
sut 변수를 사용하면 테스트 코드에서 어떤 부분이 테스트 대상인지 명확하게 식별할 수 있다. 다른 객체나 변수와 구분하기 위한 목적으로 사용된다.
가독성 향상
sut를 사용하면 테스트 코드가 더 읽기 쉽고 명확해진다. 다른 개발자들도 테스트 대상이 무엇인지 빠르게 이해할 수 있다.
유지보수 용이성
나중에 테스트 코드를 수정해야 할 때 sut 변수를 통해 테스트 대상을 변경하기 쉽다. sut 변수 하나만 수정하면 다른 코드를 수정할 필요가 없기 때문이다.
sut 변수의 사용은 테스트 코드를 작성할 때 일반적으로 사용되는 관례다. 코드를 읽는 사람들에게 테스트 대상을 명확하게 전달하고 가독성을 향상시키기 위해 사용된다.
when().thenReturn()과 BDD의 When의 차이점
Mockito는 Java 기반의 테스팅 프레임워크로, 특히 단위 테스트에 유용하다. Mockito의 `when().thenReturn()` 구문은 목 객체(mock object)의 특정 메소드가 호출될 때 반환될 값을 정의한다. 이 구문은 주로 BDD의 'Given' 단계에서 사용되며, 테스트 환경을 설정하고 목 객체의 행동을 정의하는 데 사용된다.
이와는 대조적으로, BDD의 'When' 단계는 실제로 검증하고자 하는 행동이나 메소드가 실행되는 단계이다. 예를 들어, 사용자 로그인 함수를 호출하거나, 데이터를 요청하는 것과 같은 테스트의 핵심 행위가 이 단계에서 발생한다.
Mockito의 when()
메소드는 BDD의 'When' 단계와 이름이 같지만, 역할이 다르다는 점에 주의해야 한다. Mockito에서는 테스트 환경 설정에, BDD에서는 테스트의 핵심 실행에 사용된다. 이러한 차이를 인식하고 적절하게 적용하는 것이 BDD 스타일의 테스트를 효과적으로 작성하는 데 중요하다.
BDD의 실제 적용 방법 - 컨트롤러 클래스 (통합 테스트 전략 적용)
다음은 Spring Boot에서 `OrderController`의 통합 테스트를 위한 예시다. 이 테스트는 `OrderService`와 함께 작동하는 방식을 검증한다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@DisplayName("[happy] 새 주문 생성 - 성공 시 HTTP 상태 코드 200 반환")
@Test
void createOrderTest() throws Exception {
// Given
String orderJson = "{\"customerId\":1, \"items\":[{\"productId\": 3, \"quantity\": 2}]}";
// When
mockMvc.perform(post("/orders")
.contentType("application/json")
.content(orderJson))
// Then
.andExpect(status().isOk());
}
@DisplayName("[bad] 잘못된 주문 요청 - 잘못된 데이터로 인해 HTTP 상태 코드 400 반환")
@Test
void createOrderTestWithInvalidData() throws Exception {
// Given
String invalidOrderJson = "{\"customerId\":1, \"items\":[]}"; // 유효하지 않은 주문 데이터
// When
mockMvc.perform(post("/orders")
.contentType("application/json")
.content(invalidOrderJson))
// Then
.andExpect(status().isBadRequest()); // HTTP 상태 코드 400 (Bad Request) 예상
}
}
위 코드의 given, when, then을 설명해보겠다.
Given (준비 단계)
컨트롤러의 통합 테스트에서 'Given'은 HTTP 요청을 보낼 준비를 한다. 이 경우 JSON 형식의 주문 데이터(orderJson)를 정의한다.
When (실행 단계)
When' 단계에서는 실제 HTTP 요청을 보낸다. 여기서는 MockMvc를 사용하여 OrderController의 엔드포인트 /orders에 POST 요청을 보내며, 주문 데이터를 전달한다.
Then (검증 단계)
'Then' 단계에서는 요청의 결과를 검증한다. 이 테스트는 HTTP 상태 코드가 200 OK인지 확인하여 요청이 성공적으로 처리되었는지 검증한다.
@DisplayName의 사용과 중요성
@DisplayName 어노테이션은 JUnit 5에서 도입된 기능으로, 테스트 케이스에 대한 설명을 제공한다. 이 어노테이션을 사용하여 테스트 메소드에 더 읽기 쉽고 이해하기 쉬운 이름을 부여할 수 있다.
명확한 의도 전달
@DisplayName을 통해 각 테스트의 목적과 의도를 명확하게 전달할 수 있다. 이는 테스트 코드의 가독성을 높이고, 다른 개발자들이 코드를 더 빠르게 이해하는 데 도움을 준다.
시나리오 기반 접근
BDD와 함께 사용할 때, @DisplayName은 각 테스트 케이스가 어떤 시나리오를 다루는지 명확히 할 수 있다. 예를 들어, "[happy]"는 성공 시나리오, "[fail]"은 실패 시나리오를 나타낸다.
테스트 결과의 해석 용이
테스트 실행 결과가 보고될 때, @DisplayName에 제공된 설명은 결과를 해석하는 데 유용하다. 특히 복잡한 테스트 케이스에서 이는 중요한 역할을 한다.
[happy]
성공적인 상황이나 예상되는 결과를 검증하는 테스트. 예: 주문 상태가 정상적으로 업데이트됨.
[fail] 또는 [bad]
예외 상황이나 실패하는 경우를 검증하는 테스트. 예: 존재하지 않는 데이터에 대한 요청, 잘못된 입력 처리 등.
마무리
이렇게 우리 프로젝트에서는 BDD와 @DisplayName 어노테이션을 활용해 테스트 코드의 가독성을 높이는 데 집중하고 있다.
아직 적용하지 않은 더 다양한 테스트 코드 작성 방법이 많기 때문에 하나씩 공부하면서 프로젝트에 적용하면 좋을것들을 정리해야겠다.
🔻 단위 테스트에서 static 메소드를 검증하는 방법이 궁금하다면? 🔻
단위테스트에서 Static 메소드 Mock 주입 문제 해결 방법
📣 이 글은 내가 소속된 Team Chillwave에서 진행한 사이드 프로젝트에서 경험한 내용을 정리한 것이다.
다른 팀원인 "개발자의 서랍" 님의 블로그도 방문하면 도움이 될 것 같다 :)
'Spring > Spring 테스트코드' 카테고리의 다른 글
스프링 테스트 코드에서 실제 호출 @SpyBean으로 확인하기 (3) | 2023.12.31 |
---|---|
스프링 테스트 코드: @MockBean과 @Mock의 차이 (+ @InjectMocks) (1) | 2023.12.31 |
테스트 전략에서 단위 테스트와 통합 테스트의 차이점과 의존성 관리 (1) | 2023.12.25 |
헥사고날 아키텍처와 TDD로 가는 길 (1) - 레이어 별 단위 테스트, 통합 테스트 전략 선택 (4) | 2023.12.24 |
회원가입 컨트롤러 테스트(Controller Test) - RequestDto @NotBlank 필드 검증 (1) | 2023.12.22 |