박종훈 기술블로그

BDD를 소개합니다. (Introducing BDD 번역)

원문 : Introducing BDD
Daniel Terhorst-North

이 글은 테스트의 두 분파 (Classical and Mockist Testing) (마틴 파울러 - Mocks Aren’t Stubs 번역) 의 원문에서 언급되어서 번역하며 읽게 되었습니다.

참고로 이 글에서는 behavior 가 아닌 behaviour 를 사용했지만 의미의 차이는 없습니다. 더 일반적인 용어인 behavior 로 사용하겠습니다.


다양한 환경의 프로젝트에서 테스트 중심 개발(TDD)와 같은 애자일 방식을 사용하고 가르치면서 계속해서 동일한 혼란과 오해에 직면했습니다. 프로그래머들은 어디서부터 시작해야할지, 무엇을 테스트해야하고 무엇을 테스트하지 않아도 되는지, 한 번에 얼마나 테스트해야 하는지, 테스트 이름을 어떻게 지을지, 테스트 실패시 이유가 무엇인지 에 대해서 알고싶어했습니다.

TDD에 깊게 파들어갈수록, 점진적인 숙련의 과정이라기 보다는 연속된 벽에 부딪히는 느낌이 들었습니다. 고민들에 대해 해답을 얻는건 어려웠으며 누군가 답을 줬으면 하는 마음도 들었습니다.

저는 장점은 내세우고 곤란한 질문들을 피하면서 TDD를 소개 할 수 있어야 한다고 생각했습니다. 그리고 그것에 대한 나의 대답은 행동 중심 개발(BDD, behavior-driven development)입니다. BDD는 기존의 애자일 관행에서 진화한 개발 방법으로, 애자일 소프트웨어 개발에 처음 참여하는 팀들에게 애자일 개발을 더 접근 가능하고 효과적으로 만들기 위해 설계되었습니다.

시간에 지남에 따라 BDD는 애자일 분석과 자동 인수(acceptance) 테스트에 대한 더 넓은 시각을 포함하도록 화장되었습니다.

테스트 메소드 이름은 문장이여야 합니다. (Test method names should be sentences)

내가 첫번째로 영감을 얻은 순간은 내 동료였던 Chris Stevenson 가 만든 agiledox 라는 간단한 유틸리티를 본 순간이였습니다. Junit 테스트 클래스의 메소드 이름을 문장으로 출력해주는 유틸리티입니다.

예시는 다음과 같습니다.

public class CustomerLookupTest extends TestCase {
  testFindsCustomerById() {
    ...
  }

  testFailsForDuplicateCustomers() {
    ...
  }
  ...
}

이런 테스트 케이스가 있다면 아래와 같이 출력합니다.

CustomerLookup
- finds customer by id
- fails for duplicate customers
- ...

클래스 이름과 메서드 이름 모두에서 “test”라는 단어를 제거하고 메서드 이름의 카멜 케이스를 일반 텍스트로 변환했습니다. 그게 전부지만 그 효과는 놀랍습니다.

개발자들은 이를 통해 최소한 문서화 작업을 수행할 수 있다는 사실을 발견하고 실제 문장으로 테스트 메소드 이름을 작성하기 시작했습니다. 게다가 그들은 비즈니스 도메인의 언어로 메소드 이름을 작성하면 생성된 문서가 비즈니스 사용자, 분석가 및 테스터에게도 의미가 있다는 것을 발견했습니다.

간단한 문장 템플릿은 테스트 메서드를 집중하도록 합니다. (A simple sentence template keeps test methods focused)

(실제 문장으로 테스트 메소드 이름을 작성해보니) 테스트 메소드 이름이 should 라는 단어로 시작하는 관례를 발견했습니다. 클래스가 뭔가를 해야 한다는 (The class should do something) 문장 템플릿을 통해 현재 클래스에 대한 테스트를 정의하게 합니다. 이를 통해 집중을 유지시킵니다. 이름이 이 템플릿에 맞지 않는 테스트를 작성하는 경우 해당 동작이 다른 곳에 속할 수 있음을 나타냅니다.

예를들어, 입력에 대한 검증을 하는 클래스를 작성하고있다고 해봅시다. 대부분의 필드는 이름, 성 등 일반 고객 세부 정보이지만 생년월일 필드와 나이 필드도 있습니다. 나는 ClientDetailsValidatorTest 클래스에서 testShouldFailForMissingSurnametestShouldFailForMissingTitle 테스트 메소드를 작성하였습니다.

그런 다음 나이를 계산하고 비즈니스 규칙의 세계로 들어갑니다.

  • 나이와 생년월일을 모두 제공했지만 일치하지 않으면 어떻게 되나요?
  • 오늘이 생일이라면?
  • 생년월일만 있는 경우 나이를 어떻게 계산하나요?

나는 이 동작을 설명하기 위해 점점 더 번거로운 테스트 클래스 메소드 이름을 작성하고 있었기 때문에 이를 다른 것으로 넘기는 것을 고려했습니다. 이것은 AgeCalculator 라는 클래스를 만들도록 하였고 그에 따라 AgeCalculatorTest 도 만들었습니다.

나이 계산과 관련된 동작이 AgeCalculator로 이동되었으므로, 나이 계산과 관련하여 계산기와 제대로 상호 작용하는지 확인하는 것은 단 한 번의 테스트만 하면 됩니다.

만약 하나의 클래스가 두 가지 이상의 작업을 수행하는 경우, 일반적으로 그것은 다른 클래스를 도입하여 작업을 분리해야 한다는 신호로 간주합니다.

새로운 서비스가 수행하는 작업을 설명하는 인터페이스를 정의하고, 이 서비스를 클래스 생성자를 통해 전달합니다.

public class ClientDetailsValidator {
  private final AgeCalculator ageCalc;

  public ClientDetailsValidator(AgeCalculator ageCalc) {
    this.ageCalc = ageCalc;
  }
}

종속성 주입 으로 알려진 이러한 객체 연결 스타일은 모의 객체(mock)와 특히 유용합니다.

표현력 있는 테스트 이름은 테스트가 실패할 때 도움이 됩니다 (An expressive test name is helpful when a test fails)

(실제 문장으로 테스트 메소드 이름을 작성해보니) 코드를 변경하여 테스트가 실패하게 된 경우, 테스트 메서드 이름을 보고 코드의 의도된 동작을 식별할 수 있다는 사실을 발견했습니다.

일반적으로 다음과 같은 경우가 발생하였습니다.

  • 내가 잘못해서 버그를 추가하였습니다. → 해결책 : 버그를 수정합니다.
  • 의도된 동작은 여전히 관련이 있었지만 다른 곳으로 옮겨졌습니다 → 해결책 : 테스트도 옮기고 수정합니다.
  • 동작이 더 이상 올바르지 않습니다. 시스템 전체가 변경되었습니다. → 해결책 : 테스트를 삭제합니다.

후자는 애자일 프로젝트가 점점 발전함에 따라 발생될 가능성이 있습니다. 불행하게도 초보 TDD 사용자들은 테스트 코드를 삭제하는 것에 대해 마치 코드 품질이 저하된 것 같이 느끼는 선천적인 두려움을 가지고 있습니다.

should 라는 단어의 절묘함은 will이나 shall 과 같은 대안을 비교해 보았을 때 알 수 있습니다. should 는 암묵적으로 테스트의 전제를 의심할 수 있게 해줍니다: “정말로 그래야 할까요?(should it? really?)”
이를 통해 도입한 버그로 인해 테스트가 실패하는지 아니면 단순히 시스템 동작에 대한 이전 가정이 이제 올바르지 않기 때문에 테스트가 실패하는지 판단하기 쉽게 만들어 줍니다.

“행동”은 “테스트”보다 더 유용한 단어입니다 (“behavior” is a more useful word than “test”)

이제 “테스트”라는 단어와 각 테스트 방법 이름에 대한 템플릿을 제거하는 도구인 agiledox가 생겼습니다. TDD에 대한 사람들의 오해는 거의 항상 “테스트”라는 단어로 돌아온다는 생각이 갑자기 떠올랐습니다.

그렇다고 테스트가 TDD의 본질이 아니라는 것은 아닙니다. 메서드의 결과 집합은 코드가 제대로 작동하는지 확인하는 효과적인 방법입니다. 그러나 메서드가 시스템의 행동을 종합적으로 묘사하지 않는다면, 그 메서드들은 가짜 안전감에 빠뜨리게 합니다.

저는 TDD를 다룰 때 “테스트” 대신 “행동”이라는 단어를 사용하기 시작했는데, 이것은 잘 어울릴 뿐만 아니라 코칭 중에 발생했던 질문들을 해결해주었습니다. 그렇게 저는 TDD와 관련된 질문 중 일부에 대한 답을 얻게 되었습니다. 어떻게 테스트의 이름을 지어야 하는지 묻는다면 쉽습니다. 당신이 관심 있는 행동을 문장으로 설명하면 됩니다. 얼마나 테스트 해야하는지도 논란의 여지가 없어집니다. 단 하나의 문장으로 행동을 얼마나 자세하게 묘사할 수 있는지만이 관련이 있습니다. 테스트에 실패하면 위에서 설명한 과정을 따라 진행하면 됩니다. (버그가 추가되었든, 동작이 옮겨졌든, 더 이상 테스트가 관련이 없든)

테스트에 대한 사고에서 행동에 대한 사로고의 전환이 매우 엄청나다는 것을 알게 되었고 이에 대해서 BDD 또는 행동 중심 개발이라고 부르기 시작하였습니다.

JBehave는 테스트보다 동작을 강조합니다 (JBehave emphasizes behavior over testing)

2003년쯤, 내가 한 말에 대해서 시간을 투자해야 겠다고 생각이 들었습니다. 저는 JUnit의 대체재로 JBehave 라는 도구를 개발하기 시작하였는데, 테스트 라는 단어에 대한 언급을 없대고 대신 행동을 검증하는데 중점을 둔 단어들로 바꾸었습니다. 이렇게 한 이유는 새로운 행동 주도 원칙을 엄격히 따르면 프레임워크가 어떻게 발전할 것인지 알아보기 위해서 였습니다. 또한 테스트 기반 어휘의 방해 없이 TDD와 BDD를 소개하는 좋은 교육 도구가 될 것이라고 생각했습니다.

예를들어, CustomerLookup 이라는 클래스의 동작을 정의하기 위해, CustomerLookupBehavior 이라는 클래스를 만듭니다. 이 클래스에는 should 라는 단어로 시작하는 메소드가 포함됩니다. JUnit이 테스트를 수행하는 것처럼 동작 실행기(behavior runner)는 동작 클래스를 인스턴스화하고 각 동작 메서드를 차례로 호출합니다. 진행 상황을 보고하고 마지막에 요약을 인쇄합니다.

첫 번째 이정표는 JBehave가 자체 검증을 수행하도록 만드는 것이였습니다. 저는 스스로 실행될 수 있도록 하는 동작만 추가했습니다. 모든 JUnit 테스트를 JBehave 동작으로 마이그레이션하고 JUnit과 동일한 즉각적인 피드백을 얻을 수 있었습니다.

다음으로 가장 중요한 행동을 결정하세요 (Determine the next most important behavior)

그러다가 비즈니스 가치라는 개념을 발견하게 되었습니다. 물론, 소프트웨어를 작성하는 이유에 대해서는 항상 알고 있었지만, 지금 작성하고 있는 코드의 가치에 대해서는 생각해 본 적이 없습니다.

다른 동료인 비즈니스 분석가 크리스 맷츠는 행동 주도 개발의 맥락에서 비즈니스 가치에 대해 고민하게 하였습니다.

JBehave의 자체 호스팅으로 만드는 것이 목표였기 때문에, 이에 대해 집중하는 정말 유용한 방법은 “시스템이 아직 수행하지 않은 다음으로 가장 중요한 단계는 무엇인가?” 라고 묻는 것이였습니다.

이 질문을 하려면 아직 구현하지 않은 기능의 가치를 파악하고 우선순위를 지정해야 합니다. 또한 동작 메서드 이름을 공식화하는데 도움이 됩니다.
시스템은 X(의미 있는 행동)를 수행하지 않은 상태이지만, X는 중요하고 결국 X 를 해야합니다.(should do X). 따라서 다음 행동 메서드는 간단히 다음과 같을 것입니다.

이를 표현하면 다음과 같을 것입니다.

public void shouldDoX() {
  // ...
}

이제 나는 또 다른 TDD 질문 중 하나인, 어디서부터 시작해야 하는지에 대한 답을 얻었습니다.

요구사항도 행동입니다. (Requirements are behavior, too)

이 시점에는 나는 TDD의 작동 방식을 이해하도록 설명하는데 도움이 되는 프레임워크와 내가 직면했던 곤란한 질문들에 대처할 수 있는 접근 방식을 갖게 되었습니다.

우리는 행동 주도적인 사고를 요구사항 정의에 적용하기로 적용하기로 하였습니다. 복석가, 테스터, 개발자 및 비즈니스를 위한 일관된 어휘를 개발할 수 있다면, 기술 담당자가 비즈니스 담당자와 대화할 때 발생하던 모호함과 잘못된 의사소통을 어느정도 제거할 수 있을 것이라고 생각했습니다.

BDD는 분석을 위한 유비쿼터스 언어를 제공합니다. (BDD provides a “ubiquitous language” for analysis)

이 무렵 Eric Evans는 베스트셀러 책인 Domain-Driven Design 을 출판하였습니다. 여기에서 비즈니스 도메인을 기반으로 유비쿼터스 언어를 사용하여 시스템을 모델링하여 비즈니스 어휘가 코드베이스에 바로 스며드는 개념을 설명합니다.

저는 분석 프로세스 자체를 위해 유비쿼터스 언어를 정의하려고 노력하고 있다는 것을 깨달았습니다.

회사 내에서 일반적으로 사용하는 스토리 템플릿은 다음과 같습니다.

As a [X]
I want [Y]
so that [Z]
[X] 로서
[Y] 를 원합니다.
[Z] 가 되도록

여기서 Y는 기능, Z는 기능의 이점 또는 가치, X는 이점을 누릴 사람(또는 역할)입니다.

이 방식의 강점은 이야기를 정의할 때 이야기 전달의 가치를 강제로 확인하게 된다는 것입니다. 이야기에 실질적인 비즈니스 가치가 없다면 종종, “[어떤 기능]을 원하는 이유는 [그냥 필요하니까]” 와 같은 식으로 표현됩니다. 이와 같은 난해한 요구사항 중 일부를 쉽게 제외할 수 있습니다.

스토리의 동작은 단순히 인수 기준일 뿐입니다. 시스템이 모든 인수 기준을 충족하면 올바르게 작동하는 것입니다. 그렇지 않다면 그렇지 않습니다. 그래서 우리는 스토리의 인수 기준을 포착(capture) 하기 위한 템플릿을 만들었습니다.

템플릿은 분석가에게 인위적이거나 제약적인 느낌을 주지 않을 만큼 느슨해야 하지만, 스토리를 구성요소로 나누고 자동화할 수 있을 만큼 충분히 구조화 되어 있어야 했습니다.

우리는 다음과 같은 형식을 취하는 시나리오 측면에서 인수 기준을 설명하기 시작했습니다.

Given : some initial context (the givens),
When : an event occurs,
Then : ensure some outcomes.
Given : 주어진 초기 맥락에서,
When : 이벤트가 발생하면,
Then : 몇 가지 결과를 보장합니다.

ATM 기계를 예로 사용해 보겠습니다. 스토리 카드 중 하나는 다음과 같습니다.

Title: Customer withdraws cash**

As a customer,
I want to withdraw cash from an ATM,
so that I don't have to wait in line at the bank.
제목 : 고객이 현금을 인출**

_고객_으로서,
_현금자동입출금기에서 현금을 인출_하고 싶습니다,
_은행에서 줄을 서서 기다릴 필요가 없도록_.

이 이야기를 언제 (어느 상태에서) 전달했는지 알 수 있을까요? 그렇기 때문에 고려해야할 몇가지 기나리오 들이 있습니다.

  • 계좌에 잔고가 있는 경우
  • 계좌가 한도 이내로만 인출이 가능한 경우
  • 계좌가 한도를 초과하여 인출이 불가능한 경우

그 외에도 다양한 시나리오가 있을 수 있습니다.

첫번 째 시나리오를 given-when-then 템플릿으로 나타내면 다음과 같습니다.

Scenario: Account is in credit

Given the account is in credit
And the card is valid
And the dispenser contains cash
When the customer requests cash
Then ensure the account is debited
And ensure cash is dispensed
And ensure the card is returned
시나리오: 계좌에 잔고가 있습니다.

Given
계정에 잔고가 있습니다
그리고 카드가 유효합니다
그리고 디스펜서에 현금이 들어있습니다
When
고객이 현금 인출을 요청하면
Then
계좌에 인출한만큼 돈이 빠졌는지 확인합니다.
그리고 돈이 지급되었는지 확인합니다.
그리고 카드가 나왔는지 확인합니다.

여러 주어진 결과나 여러 결과를 자연스럽게 연결하기 위해 “And(그리고)”를 사용하는 것에 주목하세요.

두번째 시나리오는 다음과 같이 나타낼 수 있습니다.

Scenario: Account is overdrawn past the overdraft limit

Given the account is overdrawn
And the card is valid
When the customer requests cash
Then ensure a rejection message is displayed
And ensure cash is not dispensed
And ensure the card is returned
시나리오: 한도를 초과하여 계좌가 인출되었습니다

Given
계정이 초과 인출되었습니다.
그리고 카드가 유효합니다
When
고객이 현금 인출을 요청하면
Then
거부 메시지가 표시되는지 확인합니다
그리고 돈이 지급되지 않았는지 확인합니다.
그리고 카드가 나왔는지 확인합니다.

동일한 사건을 기반으로 하여 몇가지 공통된 부분들이 있습니다. Given When Then 을 재사용하여 이점을 얻고자 합니다.

이 부분은 한글로 번역하니 원문의 느낌을 살리기가 어려워 둘 다 적어두었습니다.

인수 기준은 실행 가능해야 합니다. (Acceptance criteria should be executable)

시나리오의 조각들인 조건, 이벤트, 결과 들은 코드로 직접 표현될 수 있을 만큼 세분화 되어 있습니다.

JBehave는 시나리오의 조각들을 자바 클래스에 직접 매핑할 수 있게 하는 객체 모델을 제공합니다.

각각의 조건을 아래와 같이 표현하고

public class AccountIsInCredit implements Given {
  public void setup(World world) {
    ...
  }
}

public class CardIsValid implements Given {
  public void setup(World world) {
    ...
  }
}

하나의 이벤트를 아래와 같이 표현할 수 있습니다.

public class CustomerRequestsCash implements Event {
  public void occurIn(World world) {
    ...
  }
}

결과에 대해서도 동일한 방식을 적용합니다.

그런 다음 JBehave는 이것들을 연결하여 실행합니다. JBehave는 객체를 저장할 공간인 “월드(world)”를 생성하고, 월드에 각 주어진 조건들을 차례대로 전달하여 월드의 상태를 설정합니다. 그런 다음 이벤트를 월드에서 실행합니다, 마지막으로 결과에 대한 제어를 전달합니다.

각 조각을 나타내는 클래스를 통해 해당 조각을 다른 시나리오나 이야기에서 재사용할 수 있게 합니다. 처음에는 목 객체를 사용하여 계좌를 통장 잔녹가 있는 상태로 설저하거나 카드를 유효한 상태로 설정합니다. 이는 행동 구현의 시작점이 됩니다. 어플리케이션을 구현하면서 주어진 조건과 결과는 실제로 구현한 클래스를 사용하도록 변경됩니다. 따라서 시나리오가 완료되면, 실질적인 end to end 기능 테스트로 발전합니다.

BDD의 현재와 미래 (The present and future of BDD)

많은 동료들이 다양한 실제 프로젝트에서 BDD 기술을 사용해 왔으며 이 기술이 매우 성공적이라는 것을 알았습니다. BDD는 많은 분들의 도움으로 발전해 왔습니다.

이 부분에서 JBehave에 대한 내용은 포함하지 않았습니다.