테스트 피라미드와 단위의 범위
개요
TDD로 만들어지는 테스트는 대부분 빠른 저수준 테스트다. 그런데 저수준 테스트만으로는 조립된 시스템이 동작한다는 보장이 없고, 반대로 고수준 테스트만 쌓으면 빌드가 느려지고 잘 깨진다. 어떤 종류의 테스트를 어떤 비율로 둘 것인가를 다루는 것이 테스트 피라미드다.
이 질문은 TDD 개요에서 정리한 “TDD is dead” 논쟁이 최종적으로 수렴한 지점이기도 하다. 쟁점은 TDD를 하느냐 마느냐가 아니라 테스트 포트폴리오를 어떻게 구성하느냐였다.
피라미드
핵심 주장은 한 문장이다. GUI를 통과하는 고수준 테스트보다 저수준 단위 테스트가 훨씬 많아야 한다.
적게"] --> S["Service / API 계층
중간"] S --> U["Unit
많이"]
위로 갈수록 느리고 비싸며, 아래로 갈수록 빠르고 싸다. 피라미드가 뒤집혀 고수준 테스트가 대부분을 차지하는 상태를 아이스크림 콘(ice-cream cone) 안티패턴이라고 부른다.
용어의 출처는 Mike Cohn의 2009년 책 Succeeding with Agile이다. 책에서는 “Test Automation Pyramid"로 불렀지만 실제로는 그냥 “테스트 피라미드"로 통용된다. Cohn은 2003~2004년 Lisa Crispin과의 대화에서 처음 그렸고 2004년 스크럼 모임에서 소개했다. Jason Huggins가 2006년경 독립적으로 같은 아이디어에 도달했다.
UI를 통과하는 테스트가 문제인 이유
애플리케이션을 UI로 구동하는 테스트 자동화는 초기에는 잘 동작한다. 기록·재생(record-playback) 도구를 쓰면 프로그래밍 지식이 없어도 테스트를 만들 수 있다. 문제는 그다음부터다.
| 문제 | 내용 |
|---|---|
| 느림 | 빌드 시간이 늘어난다 |
| 환경 제약 | 자동화 도구 라이선스가 설치된 특정 장비에서만 돌아간다. headless 실행이 어려워 배포 파이프라인에 넣기 곤란하다 |
| 취약함 | 시스템을 조금만 개선해도 다수의 테스트가 깨지고 전부 다시 기록해야 한다 |
| 비결정성 | 종단 테스트는 비결정적으로 실패하기 쉽고, 그러면 테스트 자체에 대한 신뢰가 무너진다 |
기록·재생 도구를 버리면 취약성은 줄지만 테스트 작성이 어려워진다. Fowler는 기록·재생 도구가 변경에 저항하고 유용한 추상화를 방해하므로 어떤 종류의 자동화에도 거의 나쁜 선택이라고 본다. 쓸모가 있다면 스크립트 조각을 생성한 뒤 정식 프로그래밍 언어로 편집하는 용도 정도다.
중간층 - Subcutaneous Test
피라미드는 UI와 단위 사이에 애플리케이션의 서비스 계층을 통과하는 테스트 층을 둘 것을 주장한다. Fowler는 이것을 Subcutaneous Test(피하 테스트)라고 부른다. UI 프레임워크를 다루는 복잡함을 피하면서 종단 테스트의 이점 상당 부분을 얻는 층이다.
웹 애플리케이션이라면 이 층이 API 계층 테스트에 해당하고, 피라미드 꼭대기의 UI 층이 Selenium 같은 도구를 쓰는 테스트에 해당한다.
고수준 테스트는 2차 방어선이다
Fowler의 운영 규칙 하나가 이 배치의 성격을 잘 보여준다.
고수준 테스트에서 실패가 나면, 기능 코드에 버그가 있는 것만이 아니라 단위 테스트가 누락되었거나 잘못된 것이기도 하다.
그래서 고수준 테스트가 드러낸 버그를 고치기 전에 먼저 그 버그를 단위 테스트로 재현한다. 그 단위 테스트가 이후로 그 버그가 되살아나지 않게 붙잡는다. 고수준 테스트는 버그를 잡는 주된 수단이 아니라, 하위 테스트가 놓친 것을 알려주는 감지기 역할을 한다.
피라미드가 성립하지 않는 경우
피라미드는 broad-stack 테스트가 집중된 테스트에 비해 비싸고 느리고 깨지기 쉽다는 가정 위에 서 있다. Fowler 본인이 각주로 단서를 달아둔 부분이다.
고수준 테스트가 빠르고, 신뢰할 수 있고, 수정 비용이 싸다면 저수준 테스트는 필요 없다.
즉 피라미드는 목적이 아니라 비용 구조에서 유도된 결론이다. 비용 구조가 다르면 모양도 달라진다. 실제로 LMAX 사례처럼 종단 테스트가 크고 가치 있는 역할을 한 보고도 있다.
피라미드보다 통합 테스트를 늘리고 단위 테스트를 줄이는 편이 낫다는 반론도 있는데, Fowler는 이 차이가 대체로 “단위 테스트"를 서로 다르게 정의하기 때문에 생기는 착시일 것으로 본다.
흔한 혼동 - 세 가지는 직교한다
팀들이 자주 뭉뚱그리는 세 개념이 있다. 이들은 서로 독립적인 특성이다.
- 종단(end-to-end) 여부 - 시스템 전체를 통과하는가
- UI 테스트 여부 - 화면을 거치는가
- 고객 대면(customer-facing) 여부 - 고객이 읽고 쓸 수 있는 형태인가
리치 JavaScript UI는 UI 동작 대부분을 Jasmine 같은 도구의 JavaScript 단위 테스트로 검증하는 것이 맞다. UI 테스트라고 해서 종단 테스트일 필요가 없다. 반대로 복잡한 비즈니스 규칙은 고객 대면 형식으로 작성하되 관련 모듈에서만 단위 테스트처럼 실행할 수 있다.
“단위"란 무엇인가
단위 테스트는 소프트웨어 개발에서 가장 흔하게 쓰이면서 가장 정의가 흐린 용어다. Fowler는 한 테스트 전문가가 “교육 과정 첫날 오전에만 단위 테스트의 정의 24가지를 다룬다"고 말한 일화를 인용한다.
정의는 갈리지만 공통 요소는 셋이다.
- 저수준이다. 시스템의 작은 일부에 집중한다
- 프로그래머 자신이 평소 쓰는 도구로 작성한다. 단위 테스트 프레임워크를 쓴다는 점만 다르다
- 다른 종류의 테스트보다 현저히 빠르다
무엇을 하나의 단위로 볼지는 상황에 달렸다. 객체지향 설계는 클래스를 단위로 보는 경향이 있고, 절차적·함수형 접근은 함수 하나를 단위로 보기도 한다. Fowler는 클래스에서 출발하되 밀접하게 관련된 클래스 묶음을 하나의 단위로 다루는 일이 잦고, 드물게는 클래스 안의 메서드 일부만을 단위로 잡는다고 적었다. 어떻게 정의하든 크게 중요하지 않다.
Solitary와 Sociable
정의보다 중요한 구분이 이것이다. Jay Fields가 만든 용어다.
| 구분 | 협력 객체 | 성격 |
|---|---|---|
| Solitary | 전부 테스트 더블로 대체 | 다른 클래스의 결함이 이 테스트를 깨뜨리지 않는다 |
| Sociable | 실제 객체를 그대로 사용 | 다른 클래스의 결함이 이 테스트도 깨뜨린다 |
주문 클래스의 가격 계산 메서드가 상품과 고객 클래스의 함수를 호출한다고 하자. Solitary를 선호한다면 고객 클래스의 결함이 주문 클래스 테스트를 실패시키는 것을 원하지 않으므로 협력 객체를 테스트 더블로 바꾼다.
90년대 xunit 테스트가 시작될 때는 협력이 곤란한 경우(원격 신용카드 검증 시스템 같은)가 아니면 solitary로 갈 이유가 없다고 봤다. 이웃 테스트가 같이 깨져도 실제 결함을 추적하는 데 어려움이 없었기 때문이다. Sociable 단위 테스트를 쓴 것이 XP 진영이 “단위 테스트"라는 용어를 오용한다고 비판받은 이유 중 하나였다. 이에 대한 Fowler의 답은 명확하다. 다른 모든 것이 정상 동작한다고 가정하고 하나의 단위의 행동을 검증하는 테스트이므로 단위 테스트가 맞다.
2000년대에 Mock 객체와 목 프레임워크가 부상하면서 solitary가 돌아왔고, 두 학파가 갈렸다. 이 구분이 Classicist vs Mockist와 직결된다. Mockist는 solitary를 고수하고, Classicist는 sociable을 선호한다.
Classicist라도 협력이 곤란하면 테스트 더블을 쓴다. 원격 서비스의 비결정성을 제거하는 데는 대체할 수단이 없다. 일부 Classicist는 DB나 파일시스템 같은 외부 자원과의 협력은 전부 더블로 대체해야 한다고 주장하는데, Fowler는 이것을 유용한 지침으로 보되 절대 규칙으로 다루지는 않는다. 해당 자원과의 통신이 안정적이고 충분히 빠르다면 단위 테스트에서 직접 써도 안 될 이유가 없다.
속도와 스위트 분리
단위 테스트의 공통 성질(작은 범위, 프로그래머가 직접 작성, 빠름)은 프로그래밍 중에 아주 자주 실행할 수 있다는 결과로 이어진다. Fowler는 컴파일할 만한 코드가 생길 때마다, 분당 여러 번 단위 테스트를 돌린다고 적었다. 방금 넣은 변경이 원인이면 찾아볼 범위가 좁기 때문이다.
이렇게 자주 돌리려면 전부를 돌릴 수는 없다. 그래서 스위트를 둘로 나눈다.
| 스위트 | 범위 | 실행 시점 |
|---|---|---|
| compile suite | 지금 작업 중인 부분에 해당하는 테스트만 | 컴파일할 때마다 |
| commit suite | 단위 테스트 전체 + 일부 broad-stack 테스트 | 공유 저장소에 커밋하기 전, 하루 여러 번 |
유용하지만 commit suite에 넣기에 오래 걸리는 테스트는 배포 파이프라인의 뒷단계로 보낸다.
허용 속도에 대한 절대 기준은 없다. 실제로 널리 알려진 실무자들의 기준이 크게 갈린다.
| 기준 | compile suite | commit suite |
|---|---|---|
| DHH | 몇 초 | 몇 분 |
| Kent Beck | - | 10분 이내 |
| Dan Bodart | - | 10초 이내 |
| Gary Bernhardt | 약 300ms | - |
판단 기준은 숫자가 아니라 목적이다. 자주 돌리기를 주저하지 않을 만큼 빨라야 하고, 자주 돌린다는 것은 버그를 감지했을 때 훑어볼 변경량이 충분히 적다는 뜻이다.
관련 문서
- TDD 개요 - TDD의 정의와 “TDD is dead” 논쟁
- Self-testing Code - 테스트 스위트가 만들어내는 상태와 TDD와의 구분
- Classicist vs Mockist - solitary/sociable 선택과 직결되는 두 학파
- 테스트 더블 - 협력 객체를 대체하는 다섯 가지 방법
참고 자료
- Martin Fowler, Test Pyramid (2012)
- Martin Fowler, Unit Test (2014)
- Mike Cohn, Succeeding with Agile (2009) - 피라미드의 출처
- Jay Fields, Working Effectively with Unit Tests (2014) - solitary/sociable 용어
- Google Testing Blog, Just Say No to More End-to-End Tests