BDD 패턴
사람은 "상태 - 행위 -결과" 기반으로 행동할 수 있다고 한다.
아래 그림처럼 고양이도 걷는 행위를 할 때에는 "상태 - 행위 - 결과"으로 표현할 수 있습니다.
사용자 또는 비즈니스 요구사항에 대한 시나리오를 행동 기반으로 자연어로 표현하는 개발과 테스트를 진행하는 방법입니다.
구체적인 시나리오에 따른 행동을 기술하고 이를 기반으로 테스트를 작성하여 사용자 행동 및 비즈니스 가치에 초점을 두고 테스트를 진행합니다.
예를 들어, 일반적인 method를 작성할 때 파라미터, 비즈니스 로직 등이 포함되어 있습니다.
해당 method가 동작하는 것이 실제 사용자 행동처럼 묘사해서 테스트 코드를 작성합니다.
//DB 대신 Map<String, Long>으로 계좌의 금액을 관리하는 환경이라고 가정한다.
// 입금하였을 때 계좌의 금액을 반환한다고 가정한다.
public long depositMoneyToAddress(String address, int depositMoney){
if(depositMoney < 0){
throw new IllegalArgumentException("Invalid depositMoney");
}
if(accounts.containsKey(address)){
throw new IllegalArgumentException("Address is not exist");
}
long currentMoney = accounts.get(address);
long afterMoney = currentMoney + depositMoney;
accounts.put(address, afterMoney);
return accounts.get(address);
}
위 method의 대한 테스트를 작성한다고 할 때 행동을 비유해보겠습니다.
[성공 테스트]
1. 입금 계좌번호와 입금 금액을 가지고 해당 depositMoneyToAddress의 접근한다.
public long depositMoneyToAddress(String address, int depositMoney)
2. 해당 method에서 계좌 번호의 대한 정보를 조회하여 입금을 진행한다.
long currentMoney = accounts.get(address);
long afterMoney = currentMoney + depositMoney;
accounts.put(address, afterMoney);
3. 입금이 완료되면 현재 계좌 번호의 금액과 예상한 결과와 맞는지 확인한다.
return accounts.get(address);
[실패 테스트]
1. 잘못된 입금 계좌번호와 입금 금액을 가지고 해당 depositMoneyToAddress의 접근한다.
public long depositMoneyToAddress(String address, int depositMoney)
2. 해당 method의 접근하였지만, 파라미터 유효성 검사에 맞지 않아서 예외를 던진다.
if(depositMoney < 0){
throw new IllegalArgumentException("Invalid depositMoney");
}
if(accounts.containsKey(address)){
throw new IllegalArgumentException("Address is not exist");
}
3. 예상한 예외가 나오는지 확인한다.
위처럼 method를 시작하는 행동부터 끝나는 행동의 기대까지 확인하며 실제 환경에서 행동하는 것처럼 테스트를 진행한다.
이 글에서는 BDD 방식의 코드를 작성하는 방법인 Given-When-Then, Describe-Context-It의 대해서 소개합니다.
공통 환경 설정
공통적으로 테스트할 Method를 구성합니다.
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ArrayUtils {
private ArrayUtils() {
String exceptionMessage = "Don't Create Utility class";
log.error("{}", exceptionMessage);
throw new IllegalStateException(exceptionMessage);
}
public static int intArrayIndexOf(int[] arr, int val) {
int size = arr.length;
for (int index = 0; index < size; index++) {
if (arr[index] == val) {
return index;
}
}
return -1;
}
}
테스트 코드를 행동의 대한 결과를 검증하는 도구는 아래를 따르며, 예제 코드는 Spring WebMVC 기준으로 작성됩니다.
환경 | 테스트 도구 |
Spring WebMVC | Assertj |
Spring WebFlux | Stepverifier |
Given-When-Then 방식
테스트 대상을 행동을 하나의 테스트로 설명하도록 합니다.
환경에 따른 주인공의 행동에 따른 결과를 한 눈에 파악하기 좋습니다.
용어 | 설명 |
given | 테스트 대상이 행동하기 전 상태를 정의한다. |
when | 구체화하고자 하는 행동을 정의합니다. |
then | 행동에 따른 예상되는 변화를 확인합니다. |
import static com.ch.test.util.ArrayUtils.intArrayIndexOf;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("Util 클래스")
class GivenWhenThenTest {
@Test
@DisplayName("intArrayIndexOf 메서드에서 배열의 존재하는 값일 떄 인덱스를 반환한다.")
void When_Exist_Number_Expect_Index(){
//given
int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int givenNumber = 3;
//when
int result = intArrayIndexOf(array, givenNumber);
//then
assertThat(result).isEqualTo(3);
}
@Test
@DisplayName("intArrayIndexOf 메서드에서 배열의 존재하지 않는 값일 떄 -1을 반환한다.")
void When_Not_Exist_Number_Expect_Minus_One(){
//given
int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int givenNumber = 11;
//when
int result = intArrayIndexOf(array, givenNumber);
//then
assertThat(result).isEqualTo(-1);
}
}
given
테스트 행동을 진행하기 앞선 주어지는 값들을 상황에 맞도록 선언하는 역할을 합니다.
아래 코드에서도 array, givenNumber으로 테스트를 진행할 때 필요한 정보들을 정의하고 있는 것을 확인하실 수 있습니다.
@Test
@DisplayName(“intArrayIndexOf메서드에서 배열의 존재하는 값일 때 인덱스를 반환한다.)
void When_Exist_Number_Expect_Index(){
// given
int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int givenNumber = 3;
...
}
when
테스트 대상이 행동에 대한 구체화를 진행하고 그에 대한 결과값을 얻습니다.
result에는 given에서 주어진 정보들을 기반으로 행동을 진행한 결과를 가지게 되는 것을 확인하실 수 있습니다.
@Test
@DisplayName(“intArrayIndexOf메서드에서 배열의 존재하는 값일 때 인덱스를 반환한다.)
void When_Exist_Number_Expect_Index(){
...
// when
int result = util.intArrayIndexof(array, givenNumber);
...
}
then
테스트 대상이 행동한 결과에 대해서 예상한 결과와 동일한지 검증합니다.
Assertj을 이용하여 테스트가 행동한 결과와 예상한 결과가 동일한 것인지 확인합니다.
@Test
@DisplayName(“intArrayIndexOf메서드에서 배열의 존재하는 값일 때 인덱스를 반환한다.)
void When_Exist_Number_Expect_Index(){
...
// then
assertThat(result).isEqualTo(3);
}
DCI(Describe-Context-It) 방식
테스트 대상 환경, 행동, 결과를 @Nest을 이용한 계층적인 클래스를 이용한 코드를 작성하여 구조화하고 가독성을 높입니다.
용어 | 설명 |
describe
|
테스트 대상이 공통적으로 사용하게 되는 정보를 정의한다. |
context
|
대상이 행동을 진행할 때 필요한 환경 정보를 정의한다. |
it
|
행동을 진행한 후 예상한 기대와 동일한지 확인한다. |
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class DescribeContextItTest {
@Nested
@DisplayName("intArrayIndexOf 메서드의")
class Describe_intArrayIndexOf {
private final int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int subject(int num) {
return intArrayIndexOf(array, num);
}
@Nested
@DisplayName("만약, 배열에 존재하는 정수가 주어졌을 때")
class Context_with_exist_number {
private final int givenNumber = 3;
private final int expectedIndex = 3;
@Test
@DisplayName("정수가 존재하는 배열의 인덱스를 반환한다. ")
void it_return_index() {
int result = subject(givenNumber);
assertThat(result).isEqualTo(expectedIndex);
}
}
@Nested
@DisplayName("만약, 배열에 존재하지 않는 정수가 주어졌을 때")
class Context_with_not_exist_number {
private final int givenmber = 11;
private final int expectedIndex = -1;
@Test
@DisplayName("-1을 반환합니다.")
void it_return_minus_one() {
int result = subject(givenmber);
assertThat(result).isEqualTo(expectedIndex);
}
}
}
int intArrayIndexOf(int[] arr, int val) {
int size = arr.length;
for (int index = 0; index < size; index++) {
if (arr[index] == val) {
return index;
}
}
return -1;
}
}
describe
테스트 대상의 행동에 따른 성공/실패 케이스를 진행할 때 공통적으로 사용될 주인공을 정의한다.
또한, 공통적으로 사용될 subject 함수를 이용해서 정의할 수 있습니다.
(subject 함수에 대해서는 아래에 설명되어 있습니다.)
@Nested
@DisplayName("intArrayIndexOf 메서드의")
class Describe_intArrayIndexOf {
private final int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int subject(int num) {
return intArrayIndexOf(array, num);
}
…
context
대상이 놓인 환경에서 행동을 진행하기 전 필요한 정보를 정의합니다.
@Nested
@DisplayName("만약, 배열에 존재하는 정수가 주어졌을 때")
class Context_with_exist_number {
private final int givenNumber = 3;
private final int expectedIndex = 3;
…
it
지금까지 주어진 정보를 바탕으로 테스트 대상이 행동을 진행한 뒤 기대한 내용과 동일한 결과가 나오는지 검증합니다.
…
@Test
@DisplayName("정수가 존재하는 배열의 인덱스를 반환한다. ")
void it_return_index() {
int result = subject(givenNumber);
assertThat(result).isEqualTo(expectedIndex);
}
…
subject 메소드
다수의 it 영역에서 중복되는 method 호출을 방지하기 위해서 테스트 대상을 실행하는 코드를 캡슐화하는 영역입니다.
이를 통해서 테스트를 진행하는 코드의 변환이 발생하였을 때 it에 존재하는 모든 코드를 수정하는 것이 아닌 subject 코드만 수정할 수 있도록 합니다.
예를 들어 util.intArrayIndexOf에 대한 파라미터가 추가된 경우
subject 메서드를 사용하지 않은 경우
@Test
@DisplayName("정수가 존재하는 배열의 인덱스를 반환한다.")
void it_return_index (){
//수정 필요 1
int result = util. intArrayIndexOf(array, givenNumber);
assertThat(result).isEqualTo(expectedIndex);
}
@Test
@DisplayName("-1을 반환합니다.")
void it_valid_minus_one(){
//수정 필요 2
int result = util. intArrayIndexOf(array, givenNumber);
assertThat(result).isEqualTo(expectedIndex);
}
subject 메서드를 사용한 경우
...
int subject(int val){
//수정 필요
return util. intArrayIndexOf(array, val);
}
…
@Test
@DisplayName("정수가 존재하는 배열의 인덱스를 반환한다.")
void it_return_index (){
int result = subject(givenNumber)
assertThat(result).isEqualTo(expectedIndex);
}
@Test
@DisplayName("-1을 반환합니다.")
void it_valid_minus_one(){
int result = subject(givenNumber)
assertThat(result).isEqualTo(expectedIndex);
}
...
정리
BDD 패턴의 코드를 작성하는 방식 2가지 given-when-then, describe-context-it을 알아보았습니다. 모두 BDD의 핵심 가치인 행동을 기반으로 테스트를 진행하는 공통점을 가지고 있으며 조금의 차이점이 있습니다.
given-when-then : 행동의 과정을 각 method로 구현
describe-context-it : 행동의 과정을 계층적인 클래스 구조로 구현
각자의 장단점이 있지만, 저는 describe-context-it 방식으로 구현하는 것을 선호하고 추천합니다.
테스트에 대한 성공, 실패 케이스가 많아지면 given-when-then 방식은 각 메서드로 구현하기 때문에 실행 결과를 한 눈에 파악하기 쉽지 않습니다.
하지만, describe-context-it 방식은 계층적인 구조로 나누어 표현되기 때문에 행동이 흘러가는 방향을 테스트 결과로 한 눈에 확인할 수 있다는 점에서 추천드립니다.
'JAVA' 카테고리의 다른 글
나만의 클린 코드 이야기 - 편리함도 좋지만, 위험성도 알고 써보자! Lombok (2) | 2025.06.09 |
---|---|
나만의 클린 코드 이야기 - 비즈니스에 맞는 Collection을 만들자! 일급 컬렉션! (5) | 2025.05.19 |
나만의 클린 코드 이야기 - if-else의 갇혀버린 return 문을 구출하기 위해 Early Return 패턴을 활용하자 (0) | 2025.05.07 |
나만의 클린 코드 이야기 - 부정 연산자도 메서드로 구분하자(NOT, !) (0) | 2025.04.28 |
나만의 클린 코드 이야기 - Enum의 description 변수를 사용하자 (0) | 2025.04.03 |
댓글