테스팅 트로피
고친 사람 github-actions[bot]
테스팅 트로피는 자동화 테스트를 짜는 노력을 어느 종류에 가장 많이 들일지 정해 줍니다. 부품 여럿을 붙여서 확인하는 테스트에 노력을 가장 많이 들입니다. 코드를 돌리지 않고 읽어서 잡는 검사는 맨 아래 받침으로 깝니다. 계층마다 들이는 노력을 쌓은 모양이 가운데가 불룩한 우승 트로피를 닮아서 이런 이름이 붙었습니다.
쉽고 빠른 이해
무슨 일을 하나 — 테스트를 짜는 노력을 어디에 가장 많이 들일지 정합니다. 주문 서버라면 요청을 받아 데이터베이스에 주문을 쓰기까지를 한 번에 확인하는 테스트에 노력을 가장 많이 들입니다.
왜 하나 — 함수 하나만 떼어 보는 테스트는 빠릅니다. 대신 부품끼리 만나는 곳의 실수를 못 봅니다. 화면부터 끝까지 도는 테스트는 그 실수를 봅니다. 대신 느리고, 코드를 안 고쳐도 결과가 들쭉날쭉합니다. 그 사이에서 빠르기와 믿을 만함이 가장 잘 맞는 곳에 노력을 가장 많이 들입니다.
어떻게 도나
- 오타와 타입 실수는 코드를 돌리기 전에 도구로 걸러 냅니다
- 계산이 복잡한 함수만 따로 떼어 확인합니다
- 부품을 진짜로 붙인 테스트로 주요 동작을 대부분 확인합니다
- 사용자 흐름 전체는 몇 개로만 확인합니다
대가 — 부품을 붙인 테스트는 함수 하나를 보는 테스트보다 느립니다. 실패했을 때 원인을 찾을 범위도 넓습니다. 테스트용 데이터베이스를 띄우고 비우는 준비도 따로 들여야 합니다.
그래서 데이터베이스를 읽고 쓰는 일이 대부분인 서버에 잘 맞습니다. 계산이 여러 갈래로 갈리는 코드는 단위 테스트로 촘촘히 보는 편이 쌉니다.
상세
주문을 받는 백엔드 서버 하나를 예로 삼습니다. 그 서버의 테스트를 네 계층으로 나눕니다. 그다음 트로피가 어느 계층에 노력을 가장 많이 들이는지와 그 이유를 봅니다.
테스트를 나누는 네 계층
자동화 테스트는 사람이 손으로 눌러 보는 대신 코드로 짜 두고 기계가 돌리는 테스트입니다. 코드를 고칠 때마다 몇 번이고 다시 돌릴 수 있습니다. 테스팅 트로피는 이 자동화 테스트를 네 계층으로 나눕니다.
맨 아래는 정적 검사입니다. 코드를 실행하지 않고 읽기만 해서 실수를 잡습니다. 변수 이름 오타나 숫자를 받는 변수에 문자열을 넘긴 실수가 여기서 걸립니다.
정적 검사는 도구가 맡습니다. 타입 검사기는 변수마다 맞는 종류의 값이 들어가는지 맞춰 봅니다. 린터는 선언만 하고 안 쓰는 변수처럼 실수일 가능성이 큰 코드를 짚어 줍니다.
그 위는 단위 테스트입니다. 함수나 클래스 하나를 떼어 값을 넣고 결과를 봅니다. 주문 서버라면 할인 금액을 계산하는 함수 하나가 여기 듭니다. 데이터베이스를 부르지 않아서 금방 끝납니다.
그 위는 통합 테스트입니다. 부품 여럿을 진짜로 붙여서 확인합니다. 주문 요청을 받는 코드부터 주문을 저장하는 코드, 그리고 데이터베이스까지 한 번에 돌립니다. 부품끼리 값을 제대로 넘기는지가 여기서 드러납니다.
맨 위는 종단 간 테스트입니다. 사용자가 쓰는 흐름을 처음부터 끝까지 따라갑니다. 브라우저를 띄워 상품을 담습니다. 주문 버튼을 누른 뒤 완료 화면이 뜨는지 봅니다. 서버와 데이터베이스와 결제 연동이 전부 떠 있어야 돌릴 수 있습니다.
아래 그림은 단위 테스트와 통합 테스트가 확인하는 범위를 주문 서버에 겹쳐 그린 것입니다.
flowchart TD
R["주문 요청"] --> C["요청을 받는 코드"]
subgraph 통합["통합 테스트가 한 번에 보는 범위"]
C --> S["할인 계산 함수"]
S --> W["주문을 저장하는 코드"]
W --> D[("데이터베이스")]
end
U["단위 테스트"] -.-> S
단위 테스트는 할인 계산 함수 하나만 봅니다. 통합 테스트는 요청을 받는 코드부터 데이터베이스까지 이어진 줄을 한 번에 봅니다. 함수 사이에서 값이 잘못 넘어가는 실수는 통합 테스트에서만 보입니다.
가운데가 불룩한 모양
트로피는 네 계층 가운데 통합 테스트에 노력을 가장 많이 들입니다. 종단 간 테스트에는 가장 적게 들입니다. 단위 테스트에는 통합 테스트보다 적게 들입니다. 정적 검사는 모든 코드에 걸리는 받침으로 깝니다.
계층마다 들이는 노력을 칸의 폭으로 그리면 불룩한 잔이 가는 기둥과 받침 위에 얹힌 꼴이 됩니다.
block-beta columns 7 space:3 e["종단 간"] space:3 space i["통합"]:5 space space:2 u["단위"]:3 space:2 s["받침 · 정적 검사"]:7
칸의 폭은 계층마다 들이는 노력을 나타냅니다. 몇 개를 짜라는 숫자는 정하지 않습니다. 정적 검사의 폭도 테스트 개수가 아닙니다. 도구를 켜 두면 모든 코드에 저절로 걸리기 때문에 받침으로 그립니다.
확신과 비용을 견주는 저울
트로피는 테스트 하나를 두 가지 값으로 견줍니다. 하나는 그 테스트를 짜고 돌리는 비용입니다. 다른 하나는 그 테스트가 통과했을 때 「운영에서도 돌겠다」고 믿을 수 있는 정도입니다. 뒤의 값을 줄여서 확신이라고 부르겠습니다.
테스트가 실제로 쓰이는 모습과 닮을수록 확신이 커집니다. 단위 테스트는 할인 함수가 맞다는 것만 알려 줍니다. 그 함수를 부르는 코드가 엉뚱한 값을 넘겨도 단위 테스트는 통과합니다. 통합 테스트는 요청이 들어와 주문이 저장되기까지를 진짜 부품으로 돌립니다. 그래서 통과하면 그 흐름이 돈다고 믿을 수 있습니다.
비용도 위로 갈수록 커집니다. 아래 표는 네 계층을 이 두 값으로 견줍니다.
| 계층 | 짜고 돌리는 비용 | 통과했을 때의 확신 | 주로 잡는 실수 |
|---|---|---|---|
| 정적 검사 | 거의 없다 | 가장 낮다 | 오타 · 타입이 안 맞는 값 |
| 단위 테스트 | 낮다 | 낮다 | 계산 논리의 잘못 |
| 통합 테스트 | 중간 | 높다 | 부품 사이에 값이 잘못 넘어감 · 쿼리 잘못 |
| 종단 간 테스트 | 가장 높다 | 가장 높다 | 화면과 서버가 함께 돌 때만 나는 잘못 |
통합 테스트는 비용이 중간입니다. 그런데 확신은 종단 간 테스트에 가깝습니다. 들인 비용에 비해 얻는 확신이 가장 큰 계층이라서 트로피는 여기에 노력을 가장 많이 들입니다.
테스트 피라미드와 갈리는 곳
테스트 피라미드는 단위 테스트를 가장 많이 두라고 권합니다. 통합 테스트는 그보다 적게, 종단 간 테스트는 가장 적게 둡니다. 개수를 쌓으면 아래가 넓은 삼각형이 됩니다.
트로피는 노력을 가장 많이 들이는 계층을 한 계층 위, 통합 테스트로 옮깁니다. 피라미드는 테스트 개수를 쌓은 그림이고, 트로피는 들이는 노력을 쌓은 그림이라는 점도 다릅니다.
피라미드는 통합 테스트가 단위 테스트보다 훨씬 느리다는 전제 위에 섰습니다. 트로피 쪽은 테스트 도구가 빨라져서 그 차이가 줄었다고 봅니다. 차이가 줄었다면 확신이 더 큰 쪽에 노력을 들이는 편이 낫다는 판단입니다.
트로피는 받침에 정적 검사를 더합니다. 피라미드에는 없는 계층입니다. 이 차이는 트로피가 처음 나온 분야에서 비롯됩니다.
두 모양이 같이 따르는 기준도 있습니다. 종단 간 테스트는 둘 다 가장 적게 둡니다. 느리고 결과가 들쭉날쭉한 테스트를 늘리지 않는다는 점은 같습니다. 이렇게 코드를 안 고쳐도 돌릴 때마다 결과가 바뀌는 테스트를 불안정한 테스트라고 합니다.
정적 검사가 받침에 놓인 까닭
이 모양은 자바스크립트로 화면을 짜는 프론트엔드 쪽에서 나왔습니다. 자바스크립트는 기본으로 타입을 검사하지 않습니다. 숫자를 받는 변수에 문자열을 넘기는 실수가 코드를 돌려 봐야 드러납니다.
그래서 타입 검사기와 린터를 켜는 일을 하나의 계층으로 세웠습니다. 테스트를 짜기도 전에 실수 한 무리를 걸러 내기 때문입니다.
자바처럼 컴파일러가 타입을 검사하는 언어에서는 이 계층이 이미 깔려 있습니다. 코드를 빌드하는 순간 타입이 안 맞는 값이 걸립니다. 백엔드 개발자에게 트로피의 받침이 낯설지 않은 까닭입니다. 그 위에 린터를 더하면 받침이 조금 더 두꺼워집니다.
코드 속사정보다 동작을 확인
트로피는 테스트가 무엇을 확인하느냐와도 맞물립니다. 함수를 어떻게 나눴는지는 확인하지 않습니다. 요청을 넣었을 때 어떤 응답이 나오고 무엇이 저장되는지를 확인합니다.
이렇게 짠 테스트는 동작을 두고 구조만 바꾸는 리팩터링에 잘 안 깨집니다. 할인 계산 함수를 둘로 쪼개도 주문 요청의 결과가 같으면 통합 테스트는 그대로 통과합니다. 함수마다 단위 테스트를 붙였다면 쪼갠 만큼 테스트도 고쳐야 합니다.
단위 테스트는 부품을 떼어 내려고 진짜 의존 부품 대신 가짜를 자주 끼웁니다. 이 가짜를 테스트 더블이라고 합니다. 가짜가 진짜와 다르게 굴면 테스트는 통과합니다. 그런데 운영에서는 깨집니다. 트로피는 진짜 부품을 붙이는 테스트를 늘려서 이런 어긋남을 줄입니다.
잘 맞는 서버와 안 맞는 서버
트로피가 잘 맞는 서버는 받은 요청을 데이터베이스에 쓰고 읽는 일이 대부분인 서버입니다. 이런 서버에는 따로 떼어 시험할 계산이 적습니다. 버그는 대개 쿼리와 데이터베이스가 만나는 이음매에서 납니다. 이음매를 보는 통합 테스트가 버그를 가장 많이 잡습니다.
계산이 복잡한 코드가 많으면 사정이 다릅니다. 요금을 매기는 규칙이 수십 갈래로 갈리는 서버를 떠올리면 됩니다. 갈래마다 통합 테스트를 짜면 데이터베이스를 수십 번 띄우게 됩니다. 이런 코드는 단위 테스트로 갈래를 촘촘히 보는 편이 쌉니다.
트로피를 따르면 치르는 값도 있습니다. 통합 테스트는 단위 테스트보다 느립니다. 테스트가 늘수록 전체를 한 번 돌리는 시간이 길어집니다. 실패했을 때 원인을 찾을 범위도 함수 하나보다 넓습니다.
통합 테스트는 진짜 데이터베이스가 있어야 돕니다. 테스트마다 데이터를 채우고 끝나면 비우는 준비를 따로 짜야 합니다. 이 준비가 허술하면 앞 테스트가 남긴 데이터 때문에 뒤 테스트가 실패합니다.
어느 모양을 고르든 기준은 하나입니다. 버그가 나는 곳을 가장 싸게 잡을 수 있는 계층에 노력을 가장 많이 들입니다.
관련 항목
트로피를 이루는 테스트 계층
정적 분석 · 린터 · 타입 검사 · 단위 테스트 · 통합 테스트 · 종단 간 테스트
트로피와 맞세워지는 테스트 구성 모양
테스트 피라미드 · 아이스크림 콘 안티패턴 · 테스팅 벌집 · 테스트 다이아몬드 · 모래시계 안티패턴
테스트를 나누는 다른 분류 기준
테스트 크기 · 테스트 범위 · 좁은 범위 테스트 · 중간 범위 테스트 · 넓은 범위 테스트 · 작은 테스트 · 큰 테스트
통합 테스트를 비싸게 만드는 문제
불안정한 테스트 · 테스트 실행 시간 · 테스트 환경 · 테스트 데이터 · 테스트 격리
트로피가 함께 권하는 테스트 작성 원칙
블랙박스 테스트 · 구현 세부 사항 · 리팩터링 · 테스트 더블 · 목 객체 · 스텁
정적 검사 계층을 받치는 언어 성질
타입 시스템 · 정적 타입 · 동적 타입 · 컴파일러 · JavaScript · TypeScript
테스팅 트로피가 놓이는 분야와 개발 방식
다른 이름: testing trophy · Testing Trophy · 테스트 트로피