[번역] Behavior Driven Development(BDD) and Functional Testing

Eric Elliot의 Behavior Driven Development(BDD) and Functional Testing 번역글입니다. 굳이 이 글이 아니더라도 모든 번역글은 역자의 의도와 상관없이 원문의 내용과 다르게 전달될 수 있으니 원문도 같이 보시는 걸 권해드립니다.

단위 테스트(Unit test)는 코드 단위가 애플리케이션의 나머지 부분과 격리되어 테스트되는 방법이다. 이를 통해 특정 함수, 객체, 클래스, 모듈 등을 테스트할 수 있으며, 애플리케이션의 개별 부분이 잘 작동하는지를 알아보는 데 유용하다.

하지만 단위 테스트는 이러한 코드 단위가 모여 전체 애플리케이션이 구성되었을 때에도 잘 작동하는지를 테스트하지는 않는다. 이를 위해서는 복수 개의 협동 테스트(Collaboration test)나 E2E 테스트(End-to-end test, aka System test) 등과 같은 통합 테스트(Integration test)가 필요하다.

시스템 테스트에는 행위 주도 개발(이하 BDD), 기능 테스트(Funtional test)를 포함한 여러 가지 방법론들이 있다.

image


행위 주도 개발이란 무엇인가?

BDD는 테스트 주도 개발(Test Driven Development)의 한 분야이다. BDD는 사람이 읽을 수 있는 사용자 요구사항 명세를 소프트웨어 테스트의 기반으로 사용한다. 도메인 주도 설계(Domain Driven Design)와 마찬가지로, BDD의 초기 단계는 이해관계자, 도메인 전문가, 엔지니어 간의 공유된 어휘를 정의하는 것이다. 이 단계에서는 엔티티, 이벤트, 출력 등을 정의하고, 정의한 요소들에 모두가 동의할 수 있는 이름을 지정하는 작업들이 포함된다.

그 후 실무자들은 사용자 인수 테스트(User Acceptance Test)와 같은 시스템 테스트를 작성하는데 사용할 수 있는 도메인 별 언어(Domain Specific Language)를 만들기 위해 이 어휘들을 사용한다.

각 테스트는 영어와 공식적으로 지정된 유비쿼터스 언어(모든 이해관계자가 공유하는 어휘)로 작성된 사용자 스토리를 기반으로 한다.

예를 들어 암호화폐 지갑의 이체 테스트는 아래와 같다.

스토리: 이체 후 잔액 변경

지갑 사용자로서
돈을 보내기 위해
지갑 잔액을 업데이트 해야한다.

내 잔액이 $40 이고,
친구 잔액이 $10 임을 감안할 때,
친구에게 $20을 송금하면
내 잔고는 $20이 되어야 한다.
그리고 친구는 $30이 되어야 한다.

이 언어는 소프트웨어의 UI나 목표를 달성하는 방법 보다는 고객이 소프트웨어로부터 얻어야 하는 비즈니스 가치에만 초점을 맞춘다는 것에 유의해야 한다. 대신 UX 디자인 프로세스의 시작점으로 사용할 수 있고, 이러한 종류의 사용자 요구사항을 미리 설계하면 실무자들과 고객이 어떤 제품을 만들고 있는지에 대해 동일한 입장을 취할 수 있도록 도울 수 있어 프로세스 후반에 생길 수 있는 많은 재작업들을 줄일 수 있다.

이 단계에서 아래 두 단계로 진행할 수 있다.

  1. 설명을 도메인 별 언어로 변환하여 human-readable한 설명에 machine-readable한 코드가 추가되도록 테스트에 구체적인 기술적 의미를 부여한다. (즉 BDD를 계속 진행)
  2. 사용자 스토리를 JavaScript, Rust 등과 같은 범용 언어를 활용하여 자동화된 테스트로 변환한다. (기능 테스트로 전환)

보통 어느 쪽이든 블랙박스 테스트로 처리하여 테스트 코드가 테스트 중인 기능의 구현 세부사항에 신경을 안 쓰게끔 하는 것이 좋다. 블랙박스 테스트는 화이트박스 테스트와 달리 구현 세부사항과 결합되지 않으므로 요구사항이 추가/변경되거나 코드가 리팩토링될 때 상대적으로 덜 취약하다.

BDD 지지자들은 Cucumber와 같은 도구를 사용하여 맞춤형 DSL을 만들고 관리한다.

이와 반대로, 기능 테스트 지지자들은 일반적으로 사용자 인터랙션을 시뮬레이션하고 실제 출력과 예상 출력을 비교하여 기능을 테스트한다. 웹 애플리케이션의 경우, 이는 보통 타이핑, 버튼 클릭, 스크롤, 확대/축소, 드래그 등을 시뮬레이션하기 위해 웹 브라우저와 상호작용하는 테스트 프레임워크를 사용하는 것을 의미한다.

필자(역주: Eric Elliot)는 일반적으로 BDD를 유지하기보단 사용자 요구사항을 기능 테스트로 변환하는 것을 선호한다. 애플리케이션과 BDD 프레임워크를 통합하는데 요구되는 복잡성과 여러 시스템 및 구현 언어들에 걸쳐있는 DSL 유지보수 비용이 높기 때문이다.

또한 human-readable한 DSL은 이해관계자들과의 커뮤니케이션 용도로써 고수준의 명세서에는 적합하지만, 일반적인 소프트웨어 시스템은 제품 장애를 일으키는 버그를 방지하기 위한 적절한 코드와 테스트 커버리지 생성을 위해 더 낮은 수준에서의 테스트가 필요할 것이다.

예를 들어 "친구에게 $20을 송금한다." 요구사항은 다음과 같이 변환되어야 한다.

  1. 지갑 오픈
  2. 송금 클릭
  3. 금액 입력
  4. 송금 지갑 주소 입력
  5. 송금하기 클릭
  6. 확인 모달 대기
  7. 거래확인 클릭

화면 아래의 계층에서는 송금 관련 워크플로우의 상태를 관리하며 정확한 금액이 지갑 주소로 전송되는지 단위 테스트를 필요로 할 것이다. 이보다 더 아래의 계층에서는 실제로 지갑 잔액이 적절하게 변경되었는지 확인하기 위해 블록체인 API를 활용할 것이고, 이러한 과정들은 클라이언트가 확인하기 힘들다.

이러한 서로 다른 테스트 요구사항은 각기 다른 테스트 단계에서 가장 잘 처리할 수 있다.

  1. 단위 테스트는 로컬 클라이언트 상태가 올바르게 업데이트 되고, 이에 따라 화면에 올바르게 표시되는지 테스트할 수 있다.
  2. 기능 테스트는 UI 인터랙션과 UI 상에서 사용자 요구사항이 충족되었는지 테스트할 수 있다. 이를 통해 UI 요소들이 적절히 연결되었는지도 확인할 수 있다.
  3. 통합 테스트는 API 통신이 제대로 이루어지고 있는지, 사용자 지갑 금액이 실제로 블록체인 상에 올바르게 반영되었는지 테스트할 수 있다.

필자는 하위 계층 동작은 말할 것도 없고, 최상위 계층인 UI 동작을 검증하는 모든 기능 테스트를 조금이라도 이해하고 있는 이해관계자들을 본 적이 없다. 정작 이들은 관심이 없는데 DSL을 만들고 유지보수하는데 비용을 들일 필요가 있을까? 전체 BDD 프로세스를 수행하든 안 하든, 우리가 놓치지 말아야 할 훌륭한 아이디어와 관행들이 많다.

  • 엔지니어와 이해관계자들이 사용자 요구사항과 소프트웨어 솔루션에 대해 효과적으로 의사소통을 하기 위한 공유 어휘를 정의해야 한다.
  • 소프트웨어의 특정 기능에 대한 허용 기준 및 수행 완료의 정의를 위한 사용자 스토리 및 시나리오를 작성해야 한다.
  • 사용자, 제품팀, 품질팀, 엔지니어 간의 협업을 통해 팀이 구축하고 있는 내용에 대한 합의를 도출해야 한다.

그렇다면 또 다른 시스템 테스트 중 하나인 기능 테스트는 무엇일까?


기능 테스트란 무엇인가?

기능 테스트란 용어 자체는 여러 의미를 갖고 있기 때문에 혼란스러울 수 있다. IEEE 24765는 아래 두 의미로 정의하고 있다.

  1. 시스템 또는 구성 요소의 내부 메커니즘을 무시하고 선택한 입력 및 실행 조건에 대한 응답으로 생성된 출력에만 초점을 맞춘 테스트 (예: 블랙박스 테스트)
  2. 특정 기능 요구사항을 가진 시스템 또는 구성 요소의 적합성을 평가하기 위해 수행되는 테스트

첫 번째 정의는 거의 모든 테스트 방식에 적용될 만큼 일반적이며, 이미 소프트웨어 테스터들이 완벽하게 이해하고 있는 블랙박스 테스트가 있다. 두 번째 정의는 일반적으로 앱의 기능과는 직접적인 관련이 없는 로딩 시간, UI 응답 시간, 서버 로드 테스트, 보안 침투 테스트 등 앱의 다른 특성에 중점을 둔다. 이 정의는 꽤나 모호해서 그 자체만으로는 유용하지 않다. 때문에 우리는 보통 단위 테스트, 스모크 테스트, 사용자 인수 테스트 등 어떤 종류의 테스트를 우리가 수행해야 하는지 더 구체적으로 알고 싶어한다.

이러한 이유로 필자는 IBM의 Developer Works에서 정의한 다음을 선호한다.

기능 테스트는 사용자 관점에서 작성되며, 사용자가 관심을 갖는 시스템 동작에 초점을 맞춘다.

이는 훨씬 더 명확하지만 우리가 테스트를 자동화하고 해당 테스트를 사용자 관점에서 수행할 거라면, 그것은 UI와 상호작용하는 테스트를 작성해야 함을 의미한다. 이러한 테스트는 "UI 테스트" 또는 "E2E 테스트"라는 이름으로도 불릴 수 있지만, "친구에게 돈을 송금할 수 있어야 한다."와 같은 사용자 요구사항과 직접적인 관련이 없는 스타일이나 색상 등을 테스트하는 UI 테스트 클래스가 있기 때문에 기능 테스트라는 용어의 필요성을 대체하지는 않는다.

단위 테스트(애플리케이션의 나머지 부분으로부터 독립된 함수, 객체, 클래스, 모듈 등 개별 코드 단위의 테스트)와는 대조적으로 기능 테스트는 사용자 요구사항을 충족하는지 확인하기 위한 사용자 인터페이스 테스트에 적용된다. 즉, UI와 상호작용하는 사용자의 관점에서 애플리케이션의 나머지 부분들과 통합하여 테스트하는 것이다.

필자는 개발자 관점에서의 코드 단위에 대한 단위 테스트와 사용자 관점에서의 UI 테스트에 대한 기능 테스트로 분류하고자 한다.


단위 테스트 vs 기능 테스트

단위 테스트는 보통 소프트웨어를 구현하는 개발자가 작성하고, 개발자의 관점에서 테스트한다. 기능 테스트는 사용자 인수 기준에 의해 진행되며 사용자 요구사항이 충족되는지 사용자의 관점에서 애플리케이션을 테스트해야 한다.

단위 테스트는 독립된 개별 단위의 코드를 테스트하기 위해 작성되며, 아래와 같은 이점이 있다.

  1. 단위 테스트는 시스템의 다른 부분에 종속되지 않기 때문에 매우 빠르게 실행되며, 일반적으로 대기할 비동기 I/O가 없다. 또한 보통 밀리 초 단위로 완료되기 때문에 전체 시스템이 실행될 때까지 기다리는 것보다 단위 테스트에서 결함을 찾아 수정하는 것이 훨씬 빠르고 비용이 적게 든다.
  2. 시스템의 다른 부분과 쉽게 분리하여 테스트할 수 있도록 모듈화를 하는 것이 중요한데, 이는 시스템 아키텍처에 큰 이점이 된다. 모듈화된 코드는 변경되더라도 그 영향이 특정 모듈로 국한되기 때문에 확장, 유지보수, 교체 등이 수월하다. 이러한 모듈화가 전체 애플리케이션에 적용되면 개발자가 더 쉽고 유연하게 작업이 가능하다.

반면에 기능 테스트는 아래와 같은 특징이 있다.

  1. 사용자 관점에서 동작을 테스트하기 위해 애플리케이션 내 관련된 모든 의존성을 통합하여 테스트해야 하므로 실행시간이 더 오래 걸린다. 규모에 따라 몇 시간 이상이 걸리기도 하며, 10분 이내에 완료될 수 있도록 병렬로 실행되게끔 최적화를 하는 것이 좋지만 그래도 여전히 오래 걸린다.
  2. 모듈들이 전체 시스템으로 통합되어서도 잘 동작하는지 테스트를 해야 한다. 기능 테스트는 모듈들이 모여 시스템이 완전히 통합되었을 때, 시스템 전체가 예상대로 잘 동작하는지 확인하는 시스템 테스트의 한 형태이다.

단위 테스트가 없는 기능 테스트는 지속적인 서비스 제공(Continuous Delivery)을 위한 안전성을 확신할 만큼의 코드 커버리지를 제공할 수 없다. 단위 테스트는 코드 커버리지의 깊이를 제공하며, 기능 테스트는 사용자 요구사항 테스트 케이스의 범위를 제공한다. 즉, 둘 다 필요하다.

기능 테스트는 올바른 제품을 구축하는데 도움이 된다.(Validation) 단위 테스트는 제품을 올바르게 구축하는데 도움이 된다.(Verification)

(참고 1) Validation & Verification

(참고 2) Barry Boehm은 올바른 제품을 구축하는 것과 제품을 올바르게 구축하는 것에 대한 차이를 간결하게 설명했다.


기능 테스트에서 해야할 일과 하지 말아야할 일

  • DOM을 변경하면 안된다. DOM을 변경하면 테스트 러너가 DOM이 어떻게 변경되었는지 이해하지 못할 수 있고, DOM 출력에 의존하는 다른 테스트에 영향을 끼칠 수 있다.
  • 테스트 간에 변경 가능한 상태(mutable state)를 공유하면 안된다. 기능 테스트는 너무 느리기 때문에 병렬로 실행하는 것이 매우 중요한데, 공유되어 있는 동일한 변경 가능한 상태에 대해 경쟁하는 경우, 경쟁 조건(race condition)으로 인해 상태가 결정되지 않아 테스트가 실패할 수 있다. 시스템 테스트를 실행 중이므로, 사용자 데이터를 수정하는 경우 서로 다른 테스트에 대해 서로 다른 테스트 데이터가 있어야 한다.
  • 기능 테스트를 단위 테스트와 혼합하면 안된다. 단위 테스트와 기능 테스트는 다른 관점에서 다른 시간에 실행되어야 한다. 단위 테스트는 개발자의 관점에서 작성되어야 하고, 개발자가 코드를 수정할 때마다 실행되어야 하며 3초 이내에 완료되어야 한다. 기능 테스트는 사용자의 관점에서 작성되어야 하고, 개발자의 즉각적인 피드백을 위한 비동기 I/O를 포함해야 한다. 기능 테스트 실행을 트리거하지 않고도 단위 테스트를 쉽게 실행할 수 있어야 한다.
  • 가능한 경우 헤드리스 모드를 통해 테스트를 실행하면 좋다. 브라우저를 실제로 실행할 필요가 없어 더 빠른 테스트가 가능하다. 헤드리스 모드는 대부분의 기능 테스트 속도를 높일 수 있는 좋은 방법이지만, 헤드리스 모드에서 동작하지 않는 기능들이 있어 일부 테스트는 실행할 수 없다. 일부 CI/CD 파이프라인은 헤드리스 모드에서 기능 테스트를 실행해야 하므로, 헤드리스 모드에서 실행할 수 없는 테스트가 있을 경우 QA 팀이 해당 테스트 케이스를 관리해야 한다.
  • 모바일을 포함한 여러 디바이스에서 테스트를 실행해야 한다.
  • 테스트 실패에 대한 스냅샷을 찍으면 좋다. 테스트가 잘못된 부분을 제대로 잡아내지 못할 경우 스냅샷이 유용할 수 있다.
  • 기능 테스트 실행을 10분 미만으로 유지해야 한다. 안 그러면 각 기능 테스트에 대한 작업과 잘못된 부분을 수정하는 사이에 너무 많은 지연이 발생한다. 10분이면 다음 테스트를 실행하는데 충분하며, 10분 이상 지난 후 테스트가 실패하면 다음 테스트로 넘어간 개발자가 하던 작업을 중단하고 다시 돌아와서 작업해야 하므로 비효율적이다. 이렇게 중단된 작업은 완료하는데 평균 두 배의 시간이 걸리고 에러도 대략 두 배 정도 포함된다.
  • 테스트에 실패하면 CI/CD 파이프라이닝을 멈추도록 설정하는게 좋다. 자동화된 테스트의 큰 장점 중 하나는 작동하던 기능의 버그로부터 고객들을 보호할 수 있다는 것이다. 이러한 테스트 성공여부에 따른 CI/CD 프로세스를 자동화할 수 있으므로 배포 시 버그가 없다는 확신을 가질 수 있다. CI/CD 파이프라인에서 테스트를 수행하면 개발팀의 생산성을 크게 떨어뜨리는 변화에 대한 두려움도 효과적으로 제거할 수 있다.

역주: 원문의 내용 중 TestCafe 등 특정 테스트 도구에 관한 내용은 제외하였습니다.