본문 바로가기
JAVA

BDD 패턴의 테스트 코드 작성 방식(describe-context-it, given-when-then)

by 열정적인 이찬형 2025. 6. 23.

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 방식은 계층적인 구조로 나누어 표현되기 때문에 행동이 흘러가는 방향을 테스트 결과로 한 눈에 확인할 수 있다는 점에서 추천드립니다.

댓글