Cucumber 테스트 스위트가 유지보수 악몽이 되기 전, 아무도 말해주지 않는 BDD에 관한 3가지 사실
요약
Cucumber를 활용한 BDD(Behavior-Driven Development) 도입 시 발생하는 유지보수 문제와 해결책을 다룹니다. Gherkin을 상세한 스크립트가 아닌 추상화된 동작 중심으로 작성해야 함을 강조합니다.
핵심 포인트
- Gherkin은 구현(How)이 아닌 동작(What)을 기술해야 함
- 피처 파일에 비즈니스 로직을 내장하면 유지보수 비용이 급증함
- 스텝 정의는 복잡성을 관리하는 곳이며, 피처 파일은 단순해야 함
- 추상화된 시나리오 작성을 통해 변경 사항에 유연하게 대응 가능
당신은 6년의 자동화 경험을 가지고 있고, 실행하는 데 40분이 걸리는 Cucumber 스위트를 보유하고 있지만, 세 단계의 glue code (접착 코드)를 읽지 않고서는 단 하나의 feature file (기능 파일)이 실제로 무엇을 테스트하는지 여전히 말할 수 없습니다.
저도 그런 경험이 있습니다. 당신은 "비즈니스 이해관계자들이 테스트를 읽어야 한다"는 이유로 BDD를 채택한 팀의 시니어 자동화 엔지니어입니다. 하지만 그 이해관계자들은 스프린트 2회차 이후로 테스트를 읽지 않게 되었습니다. 이제 당신은 모든 scenario (시나리오)가 작은 소설 같고, 모든 step definition (단계 정의)이 다른 세 개의 step definition을 호출하며, 원래 프레임워크를 작성했던 사람이 18개월 전에 회사를 떠난 스위트를 유지보수하고 있습니다.
당신의 Cucumber 스위트가 유지보수 악몽이 되기 전, 아무도 당신에게 말해주지 않는 사실들을 소개합니다.
첫 번째: Gherkin은 프로그래밍 언어가 아니지만, 당신은 그것을 프로그래밍 언어처럼 다루고 있습니다
당신의 feature file은 다음과 같이 보입니다:
Scenario: User completes checkout with valid payment
Given the user is logged in with email "test@example.com" and password "Password123!"
And the user has 3 items in their cart
...
이것은 BDD가 아닙니다. 이것은 우연히 Cucumber로 파싱되는 markup language (마크업 언어)로 작성된 테스트 스크립트입니다.
BDD의 원래 약속은 scenario (시나리오)가 implementation (구현)이 아닌 _behavior (동작)_을 설명하는 것이었습니다. 하지만 그 과정에서 팀들은 Gherkin을 읽기 쉬운 스크립팅 언어로 취급하기 시작했습니다. 모든 parameter (매개변수)가 인라인(inline)으로 들어갑니다. 모든 step (단계)이 구체적인 동작이 됩니다. feature file은 UI 상호작용을 하나하나 중계하는 방식이 됩니다.
그 결과는 어떨까요? 결제 흐름이 변경되면 — 그리고 반드시 변경될 것입니다 — 결제와 관련된 모든 scenario를 업데이트해야 합니다. step definition이 아니라, feature file 자체를 말이죠. 비즈니스 로직이 Gherkin 뒤에 추상화되어 있는 것이 아니라, Gherkin 안에 내장되어 있기 때문입니다.
더 나은 버전은 다음과 같습니다:
Scenario: User completes checkout with valid payment
Given the user has items ready to purchase
When the user pays with a valid credit card
...
그게 전부입니다. 단 세 줄이죠. "어떻게(how)"는 스텝 정의(step definition)에 존재합니다. "무엇을(what)"은 피처 파일(feature file)에 존재합니다. 결제 제공업체가 변경되면, 20개의 시나리오가 아니라 단 하나의 스텝 정의만 업데이트하면 됩니다.
여러분의 스텝 정의 파일은 복잡성이 머무는 곳이어야 합니다. 여러분의 피처 파일은 제품 관리자(product manager)가 스탠드업 미팅에서 읽고 고개를 끄덕일 수 있을 정도로 단순해야 합니다. 만약 여러분의 Gherkin이 테스트 스크립트처럼 보인다면, 여러분은 이미 실패한 것입니다.
두 번째 사실: 스텝 정의는 라이브러리가 아니라 의존성 그래프(Dependency Graph)입니다
어려움을 겪고 있는 모든 Cucumber 스위트에서 제가 발견하는 패턴은 다음과 같습니다:
// step_definitions/checkout_steps.ts
import { Given, When, Then } from '@cucumber/cucumber';
import { loginAsUser } from './auth_steps';
...
이것은 테스트 프레임워크로 위장한 의존성 그래프입니다. 모든 스텝 정의가 다른 스텝 정의를 임포트(import)하고 있습니다. 호출 순서가 중요해집니다. 공유된 this 컨텍스트(context)는 스텝 전반에 걸쳐 상태(state)를 축적합니다. 만약 하나의 스텝이 실패하면 시나리오 전체가 오염되며, 실패가 실패한 그 스텝 자체의 문제인지 아니면 그 이전 세 개의 스텝으로부터 축적된 상태의 문제인지 구분할 수 없게 됩니다.
반론이 있을 수 있습니다: "하지만 시나리오 전반에서 스텝을 재사용해야 합니다. 그것이 BDD의 핵심 목적 아닙니까?"
재사용에는 동의합니다. 하지만 여러분이 재사용하는 방식에는 동의하지 않습니다.
해결책은 페이지 오브젝트(page objects)나 서비스 클라이언트(service clients)를 스텝 정의로부터 완전히 분리하는 것입니다. 스텝 정의는 다른 스텝 정의를 호출하는 것이 아니라, 공유된 도메인 레이어(domain layer)를 호출하는 얇은 래퍼(thin wrapper)여야 합니다.
// domain/checkout.ts
export class CheckoutFlow {
constructor(private page: Page) {}
...
이제 여러분의 스텝 정의는 상태가 없습니다(stateless). 서로를 임포트하지 않습니다. 가변적인 컨텍스트(mutable context)를 공유하지도 않습니다. 각 스텝은 동작을 캡슐화하는 도메인 오브젝트(domain object)를 생성하거나 호출합니다. 만약 스텝이 실패한다면, 그 실패는 해당 스텝의 도메인 작업으로 격리됩니다.
스텝 정의(step definition) 파일은 얽히고설킨 임포트(import)의 거미줄이 아니라, 하나의 라우팅 테이블(routing table)이 됩니다. 이를 통해 해당 스텝에 의존하는 다른 세 개의 시나리오가 깨질까 봐 걱정할 필요 없이 스텝 정의를 삭제할 수 있습니다.
세 번째 사실: 당신의 Cucumber 스위트는 테스트 프로젝트가 아니라 문서화 프로젝트입니다
이것은 아무도 대놓고 말하고 싶어 하지 않는 사실입니다.
BDD(Behavior-Driven Development)의 주된 목적은 결코 테스트가 아니었습니다. 그것은 바로 _커뮤니케이션(communication)_에 관한 것이었습니다. 원래의 비전은 시나리오가 '살아있는 문서(living documentation)' 역할을 하는 것이었습니다. 즉, 개발자, 테스터, 그리고 비즈니스 이해관계자(stakeholders) 사이의 공유된 언어가 되는 것이었습니다. 테스트는 그 과정에서 발생하는 부수적인 효과였습니다.
하지만 지난 10년 사이, 팀들은 테스트 커버리지(test coverage)로 BDD의 성공을 측정하기 시작했습니다. "우리는 500개의 Cucumber 시나리오를 보유하고 있습니다.", "우리의 BDD 스위트는 CI(지속적 통합)에서 실행됩니다.", "우리는 90%의 Gherkin 커버리지를 달성했습니다."와 같은 식입니다.
이러한 지표 중 그 어느 것도 당신의 시나리오가 읽기 쉬운지(readable)를 알려주지 않습니다. 또한, 새로운 팀원이 피처 파일(feature file)을 열었을 때 테스트를 실행하지 않고도 시스템이 무엇을 하는지 이해할 수 있는지도 알려주지 않습니다.
여기 불편한 진실이 있습니다. 만약 당신의 Cucumber 스위트가 유지보수의 악몽이라면, 그것은 당신이 잘못된 것을 최적화했기 때문입니다. 당신은 테스트 실행 속도, 스텝 재사용성, 매개변수화(parameterization)를 최적화했습니다. 정작 _읽기(reading)_를 위한 최적화는 잊어버린 것입니다.
저는 이를 고통스럽게 배웠습니다. 저에게는 아름다운 스텝 정의를 가진 스위트가 있었습니다. 깔끔한 추상화(abstraction), 중복 제로. 하지만 제품 관리자(product manager)가 결제 흐름(checkout flow)이 어떻게 되는지 물었을 때, 저는 그것을 설명하기 위해 세 개의 피처 파일을 열고 다섯 개의 시나리오 아웃라인(scenario outlines)을 추적해야만 했습니다. 기술적으로 문서는 존재했지만, 무용지물이었던 것입니다.
해결책은 잔혹하지만 간단합니다. 시나리오를 추가할 때마다 스스로에게 물으십시오. 당신의 코드베이스를 한 번도 본 적 없는 사람이 30초 이내에 이를 이해할 수 있는가? 만약 대답이 '아니오'라면, 시나리오를 다시 작성하십시오. 스텝 정의가 아니라, 시나리오를 말입니다.
여러분의 피처 파일 (feature files)은 새로운 엔지니어가 팀에 합류했을 때 가장 먼저 읽어야 하는 것이어야 합니다. README도 아니고, 위키 (wiki)도 아닙니다. 바로 피처 파일입니다. 만약 그 파일들만으로 시스템을 이해할 수 없다면, 여러분의 BDD 스위트 (suite)는 본연의 목적을 달성하는 데 실패한 것입니다.
내일부터 해야 할 일
피처 파일 하나를 고르십시오. 가장 최악인 것으로 말입니다. 스텝 (steps)이 가장 많고, 파라미터 (parameters)가 가장 많으며, 인라인 데이터 (inline data)가 가장 많은 파일 말입니다.
시나리오당 3~5줄이 되도록 다시 작성하십시오. 모든 구체적인 값 (concrete value)은 스텝 정의 (step definition)나 테스트 데이터 팩토리 (test data factory)로 옮기십시오. 다른 스텝 정의를 임포트 (import)하는 모든 스텝 정의를 삭제하십시오. 대신 도메인 객체 (domain objects)로 대체하십시오.
스위트를 실행하십시오. 만약 통과한다면, 여러분은 단 한 번의 움직임으로 문서화를 개선하고 테스트의 유지보수성을 높인 것입니다.
그런 다음, 다음 파일에 대해서도 똑같이 반복하십시오.
당신을 위한 질문
여러분의 Cucumber 스위트가 CI에서 실행됩니다. 통과합니다. 팀은 제품을 출시하고 있습니다. 하지만 만약 여러분이 오직 피처 파일만을 사용하여 신입 사원에게 시스템의 동작을 설명해야 한다면, 5분 이내에 할 수 있습니까?
만약 대답이 '아니오'라면, 어디서부터 시작해야 할지 알 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기