테스트 러너
고친 사람 github-actions[bot]
테스트 러너는 짜 둔 테스트를 찾아 하나씩 돌리고 결과를 모아 알려 줍니다. 테스트 코드는 함수만 적어 둔 것이라 누군가 불러 주어야 돕니다. 그 부르는 일을 맡는 프로그램이 테스트 러너입니다. 개발 도구의 테스트 실행 버튼을 누르면 뒤에서 도는 것이 대개 이 프로그램입니다.
쉽고 빠른 이해
무슨 일을 하나 — 테스트를 찾아 하나씩 부르고 몇 개가 통과했는지 알려 줍니다. 테스트 두 개를 돌려 「1개 통과, 1개 실패」와 실패한 테스트의 이름을 보여 주는 식입니다.
왜 하나 — 테스트 함수는 부르는 코드가 없습니다. 러너가 없으면 테스트를 부르고 실패를 세는 코드를 사람이 매번 따로 짜야 합니다.
어떻게 도나
- 정해진 이름 규칙이나 표시를 보고 테스트를 찾습니다
- 테스트를 하나씩 부릅니다. 하나가 실패해도 멈추지 않고 결과를 적습니다
- 끝나면 결과를 모아 보여 줍니다. 하나라도 실패했으면 실패를 뜻하는 숫자를 돌려줍니다. 이 숫자를 종료 코드라고 합니다
대가 — 러너가 정한 규칙을 따라야 테스트로 잡힙니다. 이름을 잘못 지으면 그 테스트는 조용히 안 돕니다.
상세
이 절은 할인 가격을 계산하는 함수 하나와 그 테스트 두 개를 가지고 테스트 러너를 봅니다. 먼저 러너 없이 테스트를 돌려 보고, 러너가 그 빈 곳을 어떻게 메우는지 차례로 짚습니다.
여기서 테스트는 테스트 케이스 하나를 말합니다. 정해 둔 입력을 넣고 나온 값이 기대한 값과 같은지 확인하는 작은 함수 하나입니다. 「2만 원어치를 사면 1만 8천 원이 나와야 한다」를 확인하는 함수가 테스트 하나입니다.
학교 쪽지 시험에는 문제를 낸 선생님과 답을 쓰는 학생 말고 한 사람이 더 있습니다. 감독관은 시험지를 나눠 주고 시간이 끝나면 걷어 갑니다. 걷은 답안지를 정답표와 맞춰 보고 몇 장이 맞고 몇 장이 틀렸는지 적어 올립니다. 문제를 내지도 답을 쓰지도 않습니다.
이 비유에서 감독관이 테스트 러너입니다. 시험지는 테스트이고, 답을 쓰는 학생은 테스트가 확인하는 코드입니다. 문제와 정답표를 만든 선생님은 테스트를 짠 개발자입니다.
테스트 러너는 이런 테스트를 찾아 하나씩 부르는 프로그램입니다. 부른 테스트가 통과했는지 실패했는지 적어 두었다가 끝에 모아서 보여 줍니다. 테스트를 짜는 일은 러너의 몫이 아닙니다. 러너는 부르고 세는 일을 맡습니다.
러너 없이 테스트를 돌리면
이 소절은 러너 없이 테스트를 돌려 보며 러너가 왜 필요한지 봅니다. 가격 함수 price 는 2만 원 이상이면 2천 원을 깎고, 그보다 적으면 그대로 돌려줍니다.
테스트 안에서 결과를 확인하는 문장을 단언이라고 합니다. 단언은 조건이 참이면 아무 일도 하지 않습니다. 거짓이면 예외를 던져 그 테스트를 거기서 멈춥니다. 파이썬에서는 assert 문이 단언이고, 거짓일 때 AssertionError 를 던집니다.
def test_discount():
assert price(20000) == 18000 # 통과
def test_small_order():
assert price(10000) == 9000 # 실패
위 코드에서 볼 것은 둘째 테스트입니다. price(10000) 은 할인 없이 10000 을 돌려줍니다. 기대한 값 9000 과 달라서 단언이 예외를 던집니다.
두 함수는 정의만 있고 부르는 코드가 없습니다. 파일을 그대로 실행하면 아무것도 돌지 않습니다. 돌리려면 두 함수를 부르고 예외를 잡아 세는 코드를 따로 짜야 합니다.
tests = [test_discount, test_small_order]
passed, failed = 0, 0
for t in tests:
try:
t()
passed += 1
except AssertionError:
failed += 1
print(passed, failed) # 1 1
이 짧은 루프가 테스트 러너의 뼈대입니다. 테스트를 부른 뒤 단언이 던진 예외는 잡아서 실패로 셉니다. 그다음 테스트로 넘어갑니다. 예외를 잡기 때문에 한 테스트가 실패해도 나머지가 계속 돕니다.
이 루프에는 빈 곳이 넷 있습니다. 테스트 러너는 이 넷을 메운 루프입니다. 아래 표가 빈 곳과 러너가 메우는 방법을 나란히 놓습니다.
| 루프의 빈 곳 | 러너가 메우는 방법 |
|---|---|
| 테스트 목록을 사람이 적는다 | 규칙에 맞는 테스트를 스스로 찾는다 |
| 단언 말고 다른 예외가 나면 루프가 멈춘다 | 어떤 예외든 테스트 하나 단위로 잡는다 |
| 테스트 앞뒤로 준비하고 치우지 못한다 | 준비 코드와 정리 코드를 앞뒤에 불러 준다 |
| 결과를 화면에 찍기만 한다 | 성공과 실패를 숫자로 돌려주고 보고서 파일도 남긴다 |
표의 네 줄이 뒤의 소절들과 차례로 맞물립니다. 그 전에 러너가 한 번 도는 순서를 봅니다.
한 번 도는 순서
이 소절은 러너가 테스트를 한 번 돌릴 때 무슨 일이 어떤 순서로 일어나는지를 그림 하나로 봅니다. 뒤의 소절들이 이 그림의 단계를 하나씩 풉니다.
러너는 먼저 테스트를 모두 찾아 목록을 만듭니다. 그다음 테스트마다 네 일을 차례로 합니다. 준비, 호출, 결과 기록, 정리입니다.
목록이 끝나면 결과를 모아 보여 줍니다. 마지막으로 종료 코드를 돌려줍니다. 종료 코드는 프로그램이 끝나면서 돌려주는 숫자입니다. 다른 프로그램은 이 숫자를 보고 테스트가 통과했는지 가립니다.
sequenceDiagram
participant 러너
participant 테스트
러너->>러너: 테스트를 찾아 목록을 만든다
loop 목록의 테스트마다
러너->>러너: 준비 코드를 부른다
러너->>테스트: 부른다
테스트-->>러너: 정상 종료 또는 예외
러너->>러너: 결과를 적는다
러너->>러너: 정리 코드를 부른다
end
러너->>러너: 결과를 모아 보여 주고 종료 코드를 돌려준다
그림에서 볼 것은 가운데 묶음입니다. 준비부터 치우기까지가 테스트마다 되풀이됩니다. 테스트 하나가 예외를 던져도 러너는 결과를 적고 다음 테스트로 넘어갑니다.
테스트를 찾는 규칙
이 소절은 목록을 사람이 적는 첫째 빈 곳을 봅니다. 러너가 테스트를 스스로 찾는 일을 테스트 탐색이라고 부릅니다.
러너는 정해 둔 규칙에 맞는 함수만 테스트로 칩니다. 흔한 규칙은 둘입니다. 이름이 test 로 시작하는 함수를 테스트로 치는 방식이 하나입니다. 함수 위에 「이것은 테스트다」라는 표시를 붙이는 방식이 또 하나입니다.
자바에서는 이 표시를 애너테이션으로 붙입니다. 애너테이션은 코드에 붙이는 꼬리표입니다. 자바의 테스트 도구 JUnit 이라면 메서드 위에 @Test 를 붙입니다. 러너는 실행 중에 이 꼬리표를 읽어 꼬리표 붙은 메서드만 골라 부릅니다.
찾는 범위도 규칙으로 정합니다. 테스트 파일을 두는 폴더나 파일 이름 모양을 정해 두고 그 안에서만 찾습니다. 그래서 새 테스트를 쓰면 정해진 폴더에 두기만 하면 됩니다. 목록을 고칠 필요가 없습니다.
이 편리함의 대가는 규칙을 어긴 테스트입니다. 이름을 check_discount 로 지으면 test 로 시작하지 않아서 러너가 못 찾습니다. 그 테스트는 실패로도 안 잡히고 그냥 안 돕니다. 결과 화면의 테스트 개수가 예상보다 적으면 이 경우를 먼저 의심합니다.
실패와 오류를 가른다
이 소절은 단언 말고 다른 예외가 나면 루프가 죽는 둘째 빈 곳을 봅니다. 러너는 모든 예외를 잡되, 어떤 예외인지에 따라 결과를 다르게 적습니다.
앞의 루프는 AssertionError 만 잡습니다. 테스트 안에서 0 으로 나누는 일이 생기면 다른 예외가 나서 루프 전체가 멈춥니다. 뒤에 남은 테스트는 돌지도 못합니다. 러너는 어떤 예외든 테스트 하나 단위로 잡아서 이런 일을 막습니다.
잡은 예외를 두 갈래로 적는 러너가 많습니다. 단언이 거짓이라서 난 예외는 실패로 적습니다. 그 밖의 예외는 오류로 적습니다. 테스트가 끝까지 가 보지도 못하고 도중에 무너졌다는 뜻입니다.
결과는 여기에 통과와 건너뜀이 더해져 넷이 됩니다. 통과는 예외 없이 끝난 것입니다. 건너뜀은 특정 운영체제에서만 도는 테스트처럼 조건이 안 맞을 때 개발자가 일부러 빼 둔 것입니다.
| 결과 | 무슨 일이 있었나 | 먼저 볼 곳 |
|---|---|---|
| 통과 | 예외 없이 끝났다 | 없음 |
| 실패 | 단언이 거짓이었다 | 기대한 값과 나온 값 |
| 오류 | 단언 말고 다른 예외가 났다 | 예외의 종류와 그 예외가 난 줄 |
| 건너뜀 | 일부러 돌리지 않았다 | 건너뛴 이유 |
표에서 볼 것은 셋째 칸입니다. 실패와 오류는 고칠 때 먼저 보는 곳이 다릅니다. 실패는 계산이 틀린 것입니다. 오류는 테스트 준비나 환경이 무너진 경우가 많습니다.
테스트 앞뒤의 준비와 정리
이 소절은 준비와 정리를 못 하는 셋째 빈 곳을 봅니다. 테스트용 데이터베이스를 예로 듭니다.
테스트가 돌기 전에 갖춰 두어야 하는 값이나 환경을 테스트 픽스처라고 합니다. 주문 테스트라면 상품 몇 개가 들어 있는 테스트용 데이터베이스가 픽스처입니다. 픽스처를 테스트마다 손으로 만들면 같은 준비 코드가 테스트 수만큼 되풀이됩니다.
러너는 준비 코드와 정리 코드를 따로 받아 두었다가 테스트 앞뒤에 불러 줍니다. 테스트가 실패하거나 오류로 무너져도 정리 코드는 부릅니다. 그래야 앞 테스트가 남긴 데이터가 뒤 테스트의 결과를 바꾸지 않습니다.
준비는 테스트마다 할 수도 있고, 여러 테스트가 한 번 준비한 것을 나눠 쓸 수도 있습니다. 데이터베이스를 띄우는 데 몇 초가 걸리면 테스트마다 띄우기에는 느립니다.
이럴 때는 테스트 여러 개를 한 묶음으로 둡니다. 데이터베이스는 묶음 시작에 한 번 띄우고 끝에 한 번 내립니다. 여러 테스트를 한 단위로 묶어 한 번에 돌리는 이 묶음을 테스트 스위트라고 합니다.
결과를 알리는 방법
이 소절은 결과를 화면에 찍기만 하는 넷째 빈 곳을 봅니다. 러너는 결과를 사람에게 보여 주는 것과 다른 프로그램에 알리는 것을 따로 합니다.
사람에게는 요약을 보여 줍니다. 통과와 실패와 오류의 수가 먼저 뜹니다. 실패한 테스트마다 이름, 기대한 값과 나온 값, 실패한 줄이 뜹니다. 앞의 예라면 test_small_order 가 9000 을 기대했는데 10000 이 나왔다고 알립니다.
다른 프로그램에는 앞에서 본 종료 코드로 알립니다. 0 이면 성공, 그 밖의 숫자면 실패로 읽는 것이 관례입니다. 러너는 테스트가 하나라도 실패하거나 오류가 나면 0 이 아닌 숫자를 돌려줍니다.
종료 코드가 쓰이는 대표적인 곳이 지속적 통합(CI, Continuous Integration)입니다. 누가 코드를 올릴 때마다 서버가 빌드하고 테스트를 돌리는 방식입니다. 서버는 러너의 결과 화면을 읽지 않습니다. 종료 코드가 0 이 아니면 그 변경을 멈춥니다.
사람이 나중에 볼 수 있도록 결과를 파일로도 남기는 러너가 많습니다. 테스트마다 이름과 결과와 걸린 시간을 적은 보고서입니다. CI 서버는 이 파일을 읽어 어느 테스트가 언제부터 실패했는지를 화면에 보여 줍니다.
골라 돌리기와 나눠 돌리기
이 소절은 테스트가 수백, 수천 개로 늘었을 때 러너가 하는 일을 봅니다. 전부 한 줄로 차례대로 돌리면 오래 걸리기 때문입니다.
첫째는 골라 돌리기입니다. 테스트 이름이나 파일, 붙여 둔 꼬리표로 일부만 골라 러너에 넘깁니다. 가격 계산 코드를 고치는 동안에는 가격 계산 테스트만 돌려 몇 초 만에 확인합니다. 코드를 올리기 전에는 전부 돌립니다.
둘째는 나눠 돌리기입니다. 여러 테스트를 동시에 돌려 걸리는 시간을 줄이는 것을 병렬 실행이라고 합니다. 러너가 테스트를 여러 프로세스나 스레드에 나눠 줍니다. 다 끝나면 결과를 한데 모읍니다.
나눠 돌리면 테스트가 도는 순서가 정해지지 않습니다. 앞 테스트가 만든 데이터를 뒤 테스트가 꺼내 쓰면, 순서가 바뀌는 순간 뒤 테스트가 실패합니다. 이렇게 다른 테스트가 먼저 돌았는지에 결과가 달린 상태를 테스트 순서 의존성이라고 합니다. 그래서 테스트는 자기가 쓸 데이터를 스스로 준비하게 짭니다.
러너를 부르는 도구
이 소절은 개발자가 러너를 어떻게 만나는지를 봅니다. 러너를 직접 명령으로 부르는 일은 드뭅니다.
개발자는 대개 다른 도구를 거쳐 러너를 부릅니다. IDE(Integrated Development Environment, 통합 개발 환경)는 코드 편집과 실행과 디버깅을 한 프로그램에 모은 도구입니다. IDE 에서 테스트 옆의 실행 버튼을 누르면 IDE 가 러너를 부릅니다. 결과는 초록과 빨강으로 그려 보여 줍니다.
빌드 도구도 러너를 부릅니다. 빌드 도구는 컴파일과 패키징 같은 빌드 단계를 차례로 돌리는 프로그램입니다. mvn test 나 gradle test 같은 빌드 도구의 테스트 명령을 치면 컴파일을 마친 뒤 러너를 불러 테스트를 돌립니다. CI 서버가 부르는 것도 대개 이 명령입니다.
flowchart TD
A["IDE 의 실행 버튼"] --> R["테스트 러너"]
B["빌드 도구의 테스트 명령"] --> R
C["CI 서버"] --> B
R --> T["테스트 하나하나"]
그림에서 볼 것은 화살표가 모이는 곳입니다. 어디서 출발하든 테스트를 부르는 것은 러너 하나입니다. 그래서 IDE 에서는 통과하는데 CI 서버에서만 실패하면, 러너보다 두 곳의 환경 차이를 먼저 봅니다.
러너와 테스트 프레임워크
이 소절은 러너와 자주 같이 불리는 테스트 프레임워크를 가릅니다. 둘은 한 도구 안에 같이 들어 있는 일이 많아서 헷갈리기 쉽습니다.
테스트 프레임워크는 테스트를 짜는 쪽의 틀입니다. 단언을 쓰는 방법, 준비와 정리 코드를 적는 방법, 테스트라는 표시를 붙이는 방법을 정해 줍니다. 러너는 돌리는 쪽입니다. 그 틀에 맞춰 짠 테스트를 찾아 부릅니다.
JUnit 이나 pytest 는 두 일을 한 도구가 같이 합니다. 그래서 「테스트 러너」와 「테스트 프레임워크」를 같은 도구 이름으로 부르는 일이 흔합니다.
CI 러너와 다른 말
이 소절은 이름이 비슷한 다른 러너를 가릅니다. 「러너」라는 말은 테스트 말고 다른 곳에서도 씁니다.
CI 서버에서 작업을 받아 도는 프로그램이나 기계도 러너라고 부릅니다. 이 러너는 테스트만이 아니라 빌드, 배포처럼 CI 가 시키는 작업을 전부 돌립니다. 테스트 러너는 그 작업 안에서 불리는 프로그램 하나입니다. 대화에서 「러너가 죽었다」라고 하면 어느 쪽인지 먼저 확인합니다.
관련 항목
테스트 러너가 찾아 돌리는 대상
테스트 케이스 · 테스트 스위트 · 단언 · 테스트 픽스처 · 테스트 메서드 · 테스트 클래스
테스트 러너를 품은 테스트 프레임워크
테스트 프레임워크 · JUnit · pytest · unittest · Jest · xUnit · Mocha
테스트 러너를 부르는 도구
IDE · 빌드 도구 · Gradle · Maven · 지속적 통합 · 배포 파이프라인
테스트 러너가 테스트를 찾는 수단
테스트 탐색 · 애너테이션 · 리플렉션 · 명명 규칙
테스트 러너가 결과를 알리는 수단
종료 코드 · 테스트 보고서 · JUnit XML · TAP · 스택 트레이스
테스트 러너가 시간을 줄이는 방법
병렬 실행 · 테스트 샤딩 · 테스트 선택 · 테스트 격리
테스트 러너의 결과를 흐리는 문제
불안정한 테스트 · 테스트 순서 의존성 · 공유 픽스처 · 깨지기 쉬운 테스트
러너라는 이름을 함께 쓰는 실행기
CI 러너 · GitLab Runner · GitHub Actions · 태스크 러너
테스트 러너가 속하는 상위 분류
QA와 테스트 · 소프트웨어 테스트 · 자동 테스트 · 테스트 자동화
다른 이름: test runner · 테스트 실행기