BDD (Behaviour-Driven Development)에 대한 간략한 정리

BDD와 테스트 용이성에 대해서 간단히 정리가 필요해서 하는 김에 포스팅으로 올린다. 한 번에 하려고 했는데 생각보다 양이 많아서 우선 BDD에 대해서만 정리한다.

BDD가 TDD만큼 유명세를 타지는 않은 거 같은데 현업에서 두 가지 모두 사용해본 경험상 BDD를 선호한다. 여기서 BDD는 Behaviour-Driven Development의 약자고 TDD는 Test-Driven Development의 약자다. 어차피 BDD가 TDD에 기반을 두고 있다고 하는 만큼 둘이 엄청난 차이를 보이지는 않는다. 다른 xDD도 많이 있겠지만 내가 사용해본 게 이 두 가지밖에 없어서 언급할 수가 없다.

TDD 는

흔히 알려진 것처럼 TDD는 테스트를 먼저 작성하고 그 테스트를 통과시키는 코드를 작성하는 흐름을 기본으로 한다. 게다가 테스트 단위도 함수 단위로 매우 작아서 작성하는 거의 모든 함수가 테스트 대상에 포함된다. 이상적으로 보일 수 있지만 그만큼 현업에서 사용하기에는 괴리감이 있다.

프로젝트 초기에 야심 차게 TDD를 도입하여 거의 모든 함수에 대한 테스트 케이스를 준비했더라도 개발 중후반에 수정되는 내용에 대해서 깨지는 테스트 케이스를 계속 유지하면서 가져가기란 쉽지 않다. 게다가 로우 레벨의 함수를 나누고 합치고 시그니쳐를 변경하는 리팩토링이라도 하는 날에는 관련 테스트는 모두 깨지며 이를 일일이 맞춰간다는 건 쉽지 않다. 이 말이 어떤 의미인지 실제 TDD를 현업에서 사용해본 사람은 욕지기와 함께 감이 올 것이다.

그렇다면 BDD는

이제 BDD를 살짝 보자. 참고로 여기서 이야기하는 BDD는 내가 사용하면서 이해하고 있는 것이라서 진정한 BDD와 차이가 있을 수도 있다.

BDD가 TDD와 엄청난 차이를 보이지는 않는다고 위에 언급했지만, 개인적인 생각은 다음과 같다.

TDD와는 다르다 TDD와는! (이 뉘앙스를 아는가?)

BDD는 시나리오를 기반으로 테스트 케이스를 작성하며 함수 단위 테스트를 권장하지 않는다. 이 시나리오는 개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 레벨을 권장한다. 하나의 시나리오는 Given, When, Then 구조를 가지는 것을 기본 패턴으로 권장하며 각 절의 의미는 다음과 같다.

Feature : 테스트에 대상의 기능/책임을 명시한다.

Scenario : 테스트 목적에 대한 상황을 설명한다.

Given : 시나리오 진행에 필요한 값을 설정한다.

When : 시나리오를 진행하는데 필요한 조건을 명시한다.

Then : 시나리오를 완료했을 때 보장해야 하는 결과를 명시한다.

위의 내용을 개발 측면에서 더 간략하게 정리하면 테스트 대상의 상태 변화를 테스트하는 것이다.

테스트 대상은 A 상태에서 출발하며(Given) 어떤 상태 변화를 가했을 때(When) 기대하는 상태로 완료되어야 한다. (Then)

또는 Side Effect가 전혀 없는 테스트 대상이라면 테스트 대상의 환경을 A 상태에 두고(Given) 어떤 행동을 요구했을 때(When) 기대하는 결과를 돌려받아야 한다. (Then)

BDD 사용 예

이제 간단한 예를 하나 가지고 조금 더 들어가겠다. 여기 간단한 주간 반복 스케줄러에 대한 간략한 요구사항이 있다.

사용자는 주간 반복 스케줄이 시작하는 요일과 시간, 종료하는 요일과 시간 그리고 스케줄 상태를 설정할 수 있다.

스케줄은 '진행', '대기', '중지'의 상태를 가질 수 있다.

시스템은 현재 스케줄 상태와 시스템 시간을 기준으로 해당 스케줄을 수행한다.

위 요구사항을 보고 작성한 인터페이스 코드를 보자. (pseudo code)

디테일한 구현과 Test Fixture 설정 등 테스트 환경 설정에 대한 내용은 생략한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Scheduler {
private :
  SchedulerConfig config
public :
  setStartTimerHandler(handler)
  setEndTimerHandler(handler)
  setStartTimer(pendingTime)
  setEndTimer(pendingTime)
  clearStartTimer()
  clearEndTimer()
  getConfig()
  prepareTimer(baseTime)
  calculateRemainingTime(baesTime, type)
  OnTimerForStart(context)
  OnTimerForEnd(context)
}

이제 테스트 시나리오를 작성해보자. 테스트 시나리오는 단순하고 명확해야 하며 가능하면 한 번에 하나만 테스트하는 것이 좋다.

사용자가 설정한 시간에 스케줄이 시작되는지를 확인하는 시나리오를 작성하자.

사용자는 매주 화요일 10시에 시작하는 스케줄을 설정하고 스케줄러를 '대기' 상태로 설정한다.

서버는 매주 화요일 10시가 되면 스케줄을 시작해야 한다.

위 시나리오가 한 번에 하나만 테스트하는 작고 단순한 시나리오 같지만, 다음과 같이 더 작게 나누는 것이 좋다.

1. 사용자는 매주 화요일 10시에 시작하는 스케줄을 설정한다.

서버는 화요일 10시가 되면 스케줄을 시작해야 한다. (단순 실행에 대한 테스트)

2. 서버는 스케줄이 종료된 이후에 다음 스케줄을 설정된 시간에 다시 시작해야 한다. (스케줄 연속성에 대한 테스트)

위 인터페이스를 가지고 1번 테스트 시나리오에 대한 구현을 해보자. (pseudo code)

스케줄이 시작되는 것을 확인하기 위해서는 OnTimerForStart 함수가 호출되는 것을 확인하는 것으로 족하다. 이를 위해서 OnTimerForStart를 Mocking 해야한다. Scheduler를 Mocking 한 Mock Scheduler 이 있다고 가정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FEATURE("Scheduler는 설정된 시간에 스케줄을 시작한다.")
TEST(Scheduler, StartSchedule) {
  SCENARIO("스케줄이 설정된 시간에 시작된다.")
  GIVEN("스케줄이 매주 화요일 10시 시작으로 설정되어있고,")
  AND("스케줄러는 '대기' 상태이다.")
  SchedulerConfig config
  config.startDateTime = '매주 화요일 10시'
  config.status = '대기'
  MockScheduler scheduler
  WHEN("서버의 현재 시간이 2017년 4월 11일 화요일 9시 59분 59초일 때")
  DateTime baesTime = '2017년 4월 11일 9시 59분 59초'
  THEN("스케줄이 1초 후에 시작되어야 한다.")
  // OnTimerForStart이 1번 호출되어야 한다.
  // 호출되면 ReturnFromAyncCall()을 실행해서 await를 해제한다.
  EXPECT_CALL(scheduler, OnTimerForStart(_))
    .Times(1)
    .WillOnce(ReturnFromAyncCall())
  EXPECT_TRUE(scheduler.prepareTimer(baseTime)
  // 타이머 호출을 기다리기 위해서 최대 2초간 대기한다.
  EXPECT_TRUE(await(seconds(2))
}

위 테스트 케이스가 성공하려면 2초 안에 OnTimerForStart() 함수가 호출되어야 한다. 2초 안에 1번도 호출되지 않으면 실패하고 2번 이상 호출되어도 실패한다.

위 시나리오를 보니 스케줄 시작 대기 시간이 1초라는 보장이 애매해 보인다. 시간 계산에 대한 테스트 시나리오를 하나 더 추가하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
FEATURE("Scheduler는 설정된 시간에 따라서 스케줄 시작까지의 남은 시간을 계산한다.")
TEST(Scheduler, CalculateRemainingTimeForStart) {
  SCENARIO("스케줄 시작까지 남은 시간을 계산한다.")
  GIVEN("리그전 스케줄이 매주 월요일 10시 시작으로 설정되어 있다.")
  SchedulerConfig config
  config.startDateTime = '매주 월요일 10시'
  MockScheduler scheduler
  WHEN("서버의 현재 시간이 2017년 4월 9일 일요일 14시일 때")
  DateTime baesTime = '2017년 4월 9일 일요일 14시'
  THEN("2017-04-10 (월) 10시에 실행되도록 대기 시간이 72,000초로 계산되어야 한다.")
  DateTime pendingTime = scheduler.calculateRemainingTime(baseTime, FOR_START_TIME);
  EXPECT_EQ(72000, pendingTime)
}

이제 시작 시간 계산에 대한 테스트 시나리오가 추가되었고 시간 계산에 대한 내용은 이 테스트 시나리오로 보장한다.

글을 마치며

예제의 사이즈가 작아서 오해의 소지가 있을 수도 있는데 작게 작게 나눈다고 함수 단위로 나누는 것으로 오해하지 않았으면 한다. 테스트 대상이 가지고 있는 인터페이스에 대한 테스트 시나리오가 중요한 것이지 내부적으로 사용되는 모든 함수가 명시적인 테스트 대상은 아니다. 인터페이스가 변하지 않는 이상 내부적인 리펙토링을 거친 코드는 테스트 코드 수정없이 모두 통과되어야 한다.

이상 BDD에 대한 정리를 마친다. 다음에는 테스트 용이성에 대해서 정리를 할 예정이다.

제가 잘못 이해하고 있는 내용은 언제든지 알려주시면 감사하겠습니다.

(원글 : http://prostars.net/227)


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.