지난 포스팅으로 BDD 는 어떤 개발방법론이고, 어떤 면에서 이 방법이 필요한지를 정리해 봤다.
이번 포스팅을 시작으로, 직접 다양한 언어에서 BDD 를 적용하는 방법을 정리해 볼 예정이다. 첫 시간으로는 파이썬 (Python) 언어를 가지고 할 것이다. Python 라이브러리 중 하나인 behave 를 통해, BDD 테스트 프레임워크를 구성하고, 예제 시나리오를 만들어서 테스트 자동화가 어떻게 이뤄질 수 있을지 알아보자.
예제 프로젝트 생성
파이썬이 설치된 환경에서 다음과 같이 디렉터리만 만들면 된다.
mkdir behave-test && cd behave-test
나는 virtualenv 를 이용해 환경을 분리해 두도록 하겠다.
virtualenv venv
source venv/bin/activate
behave 설치
pip install behave
만 하면 된다.
features/steps
디렉터리 생성
behave 는 features/steps
를 필요로 한다. 만들어 주자.
mkdir -p features/steps
디렉터리만 만들고 곧장 behave
를 실행하면, ConfigError: No feature files in $(pwd)/features
라는 메시지가 뜰 것이다.
.feature
파일 만들기
features/hotdog.feature
라는 파일을 하나 만들어보자. 핫도그 12개에서 5개를 먹으면 반드시 7개가 남아야 한다는 내용이다. 단순 영어로 되어 있지만, 문법에 대해 더 자세히 알고 싶다면 이전 포스팅을 꼭 참고하고 오자.
Feature: eat hotdogs
Scenario: Eat 5 out of 12
Given there are 12 hotdogs
When I eat 5
Then there should be 7 remaining
이제 다시 behave
를 실행하면, 아래 구조로 되어 있는 테스트 파일이 존재하지 않다고 하면서, 스니펫 (snippet) 을 출력해 주고 내부를 구현해 달라고 한다.
$ behave
Feature: eat hotdogs # features/hotdog.feature:1
Scenario: Eat 5 out of 12 # features/hotdog.feature:3
Given there are 12 hotdogs # None
When I eat 5 # None
Then there should be 7 remaining # None
Failing scenarios:
features/hotdog.feature:3 Eat 5 out of 12
0 features passed, 1 failed, 0 skipped
0 scenarios passed, 1 failed, 0 skipped
0 steps passed, 0 failed, 0 skipped, 3 undefined
Took 0m0.000s
You can implement step definitions for undefined steps with these snippets:
@given(u'there are 12 hotdogs')
def step_impl(context):
raise NotImplementedError(u'STEP: Given there are 12 hotdogs')
@when(u'I eat 5')
def step_impl(context):
raise NotImplementedError(u'STEP: When I eat 5')
@then(u'there should be 7 remaining')
def step_impl(context):
raise NotImplementedError(u'STEP: Then there should be 7 remaining')
테스트 Python 파일 만들기
방금 출력된 내용을 단순히 복사해서 features/steps/hotdog.py
라는 파일을 만들었다. 참고로 파일 이름은 상관없지만 반드시 features/steps
디렉터리에 위치해야 한다.
from behave import *
@given(u'there are 12 hotdogs')
def step_impl(context):
raise NotImplementedError(u'STEP: Given there are 12 hotdogs')
@when(u'I eat 5')
def step_impl(context):
raise NotImplementedError(u'STEP: When I eat 5')
@then(u'there should be 7 remaining')
def step_impl(context):
raise NotImplementedError(u'STEP: Then there should be 7 remaining')
시나리오의 각 구절 (GIVEN, WHEN, THEN) 에 연결되는 기준은, 각 method 에 위치한 decorator 인 @given
, @when
, @then
에 따라 달려있기 때문에, 파일 이름은 상관없고 해당 decorator 만 수정하지 않으면 된다.
그리고 주의할 것은, 반드시 맨 위에 from behave import *
와 같이 behave
라이브러리를 import 해야 한다는 것이다.
이렇게 하고 다시 behave
를 실행하면, 예상대로 NotImplementedError
exception 이 발생하면서 traceback 이 출력되고, 테스트는 실패한 것으로 간주한다.
$ behave
Feature: eat hotdogs # features/hotdog.feature:1
Scenario: Eat 5 out of 12 # features/hotdog.feature:3
Given there are 12 hotdogs # features/steps/hotdog.py:3 0.000s
Traceback (most recent call last):
File "/home/behave-test/venv/lib/python3.6/site-packages/behave/model.py", line 1329, in run
match.run(runner.context)
File "/home/behave-test/venv/lib/python3.6/site-packages/behave/matchers.py", line 98, in run
self.func(context, *args, **kwargs)
File "features/steps/hotdog.py", line 5, in step_impl
raise NotImplementedError(u'STEP: Given there are 12 hotdogs')
NotImplementedError: STEP: Given there are 12 hotdogs
When I eat 5 # None
Then there should be 7 remaining # None
Failing scenarios:
features/hotdog.feature:3 Eat 5 out of 12
0 features passed, 1 failed, 0 skipped
0 scenarios passed, 1 failed, 0 skipped
0 steps passed, 1 failed, 2 skipped, 0 undefined
Took 0m0.000s
테스트 파일 구현하기
이제 테스트 파일 안의 method 를 구현해 보자.
from behave import *
@given(u'there are {num_hotdog} hotdogs')
def step_impl(context, num_hotdog):
context.hotdogs = int(num_hotdog)
@when(u'I eat {eaten_hotdog}')
def step_impl(context, eaten_hotdog):
context.hotdogs -= int(eaten_hotdog)
@then(u'there should be {remaining_hotdog} remaining')
def step_impl(context, remaining_hotdog):
assert context.hotdogs == int(remaining_hotdog)
decorator 에서 숫자를 나타내는 부분을 num_hotdog
같은 식으로 파라메터로 치환한 것을 주목해 달라. 이렇게 해 두면, 나중에 숫자만 바꾼 시나리오를 추가해도, 위의 세 개 method 를 재사용할 수 있는 장점이 있다.
그리고 behave 는 context
라는 파라메터를 입력으로 받을 수 있도록 지원한다. 해당 시나리오가 진행되는 동안 유효한 객체이며, 따라서 여기에 원하는 attribute 를 만들어 값을 저장하고 열람할 수 있다. 예제 시나리오에서는, 남은 핫도그 개수를 context.hotdogs
로 보관한다.
엄밀히 말하면, when 에 해당하는 method 에서 핫도그 개수를 차감하는 부분은 여기서 구현하면 안 된다. 실제 업무에 사용하려면, 여기서 ‘실제로 구현한 로직’ 을 호출해야 한다. 그래야 검증을 할 수 있으니 말이다!
다시 behave
를 실행하면, 이제야 모든 테스트가 통과되었음을 알 수 있다!
$ behave
Feature: eat hotdogs # features/hotdog.feature:1
Scenario: Eat 5 out of 12 # features/hotdog.feature:3
Given there are 12 hotdogs # features/steps/hotdog.py:3 0.000s
When I eat 5 # features/steps/hotdog.py:8 0.000s
Then there should be 7 remaining # features/steps/hotdog.py:12 0.000s
1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
3 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.000s
숫자만 바꾼 시나리오를 추가하면?
시나리오만 추가하더라도 이미 재사용이 가능한 형태로 테스트 method 를 구현했기 때문에, 아래처럼 성공적으로 작동한다.
$ behave
Feature: eat hotdogs # features/hotdog.feature:1
Scenario: Eat 5 out of 12 # features/hotdog.feature:3
Given there are 12 hotdogs # features/steps/hotdog.py:3 0.000s
When I eat 5 # features/steps/hotdog.py:8 0.000s
Then there should be 7 remaining # features/steps/hotdog.py:12 0.000s
Scenario: Eat 10 out of 20 # features/hotdog.feature:8
Given there are 20 hotdogs # features/steps/hotdog.py:3 0.000s
When I eat 10 # features/steps/hotdog.py:8 0.000s
Then there should be 10 remaining # features/steps/hotdog.py:12 0.000s
1 feature passed, 0 failed, 0 skipped
2 scenarios passed, 0 failed, 0 skipped
6 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.001s
마치며
이제 이 테스트 케이스를 구현할 때 mock 라이브러리를 통해 mocking 을 하면, 기능 검증을 충분히 할 수 있을 것이다. 시나리오를 추가하는 것은 개발자 뿐만이 아닌 비 개발자나 QA 도 할 수 있어야 하고, 개발자는 그렇게 푸시된 새로운 시나리오를 보고 커버리지를 달성하기 위해 테스트에 좀 더 집중할 수 있을 것이다.
한 가지 맹점은, 기존 시나리오가 바뀌면 테스트 케이스 개발 과정에서 엄청난 고통이 뒤따를 수 있단 것이다. 따라서 (전에도 말했지만) 항상 시나리오는 합의 하에 신중히 추가되어야 한다.
다음 시간에는 golang 으로 똑같은 시나리오를 테스트 해 볼 예정이다.