정의
테스트 오라클(test oracle) 은 어떤 입력에 대한 실행 결과가 옳은지 그른지를 판정해 주는 기준이다. 테스트 코드 그 자체가 아니라, 테스트 코드가 참조하는 정답의 출처를 가리킨다.
테스트는 두 부분으로 나뉜다. 프로그램을 특정 상태로 몰아넣는 입력 생성, 그리고 그 결과가 옳은지 정하는 판정이다. 앞은 자동화가 쉽지만 뒤는 어렵고, 이 어려움을 소프트웨어 공학에서는 오라클 문제(oracle problem) 라고 부른다.
왜 필요한가
오라클을 명시하지 않으면 매우 흔한 사고가 일어난다. 먼저 구현을 만들고, 그 구현이 실제로 내놓는 값을 그대로 기대값에 적어 넣는 것이다.
// 구현을 한 번 돌려 보고 나온 값을 그대로 붙여 넣은 테스트
expect(formatDiscount(1000, 0.1)).toBe("900원");
이 테스트는 항상 통과한다. 그리고 아무것도 검증하지 않는다. 반올림 규칙이 틀렸든 통화 표기가 틀렸든, 구현이 그렇게 동작하기 때문에 기대값도 그렇게 적혔을 뿐이다. 회귀는 잡지만 최초의 오류는 절대 잡지 못하는 테스트가 된다.
코드를 생성 도구에 맡길 때 이 문제는 더 커진다. 도구는 기존 코드, 기존 테스트, 현재 화면 동작을 전부 읽을 수 있어서 "지금 이렇게 동작하니 이게 요구사항이겠지"라고 추론하기 쉽다. 그래서 무엇이 정답인지 정할 권한을 어디에 두는지를 먼저 못 박아야 한다.
동작 원리
오라클은 정답을 어디서 얻느냐에 따라 여러 종류로 나뉜다.
| 종류 | 정답의 출처 | 한계 |
|---|---|---|
| 명세 오라클 | 승인된 요구사항·수용 기준 문서 | 문서 위치와 버전을 고정하지 않으면 기준이 다시 흔들린다 |
| 인간 오라클 | 사람이 결과를 직접 보고 판정 | 느리고 재현이 어렵다. 판정 결과를 글로 남겨야 자동화로 넘어간다 |
| 파생 오라클 | 이전 버전 출력, 참조 구현, 운영 로그 | 이전 버전의 버그를 정답으로 승격시킬 위험 |
| 암시적 오라클 | 크래시, 미처리 예외, 데드락처럼 누가 봐도 틀린 것 | 도메인 정확성은 판정하지 못한다 |
| 메타모픽 오라클 | 입력 사이의 관계에서 출력 사이의 관계를 유도 | 정답값 자체를 모르는 도메인에서만 필요 |
정답값을 계산할 수 없는 도메인에서는 메타모픽 관계가 실질적인 대안이 된다.
// 정답 목록을 몰라도 검증할 수 있는 관계
const once = sortByScore(items);
const twice = sortByScore(shuffle(once));
expect(twice).toEqual(once); // 입력 순서가 결과를 바꾸면 안 된다
실무 적용
핵심은 판정 기준을 테스트보다 먼저 글로 적고, 각 항목에 출처를 붙이는 것이다.
| # | 상황(Given) | 동작(When) | 결과(Then) | 절대 금지(Never) | 출처 |
| --- | ---------------- | ----------- | ---------------- | ----------------------- | ---------------- |
| 1 | 목록이 비어 있음 | 페이지 진입 | 안내 문구 노출 | 스피너가 계속 도는 상태 | 기획서 3.2절 |
| 2 | 요청이 5초 초과 | 자동 취소 | 재시도 버튼 노출 | 무한 대기 | 사용자 확인 답변 |
출처를 못 붙이는 행은 빈칸으로 두고 확인을 요청한다. 관례로 채우는 순간 그 행은 검증이 아니라 추측이 된다.
작업이 길어지면 이 표를 파일로 저장하고 내용 해시를 기록해 두는 방법이 유효하다. 중간에 기준이 바뀌면 사람이 눈치채기 전에 도구가 먼저 감지한다.
shasum -a 256 docs/acceptance/checkout.md
트레이드오프
기준을 먼저 문서로 만드는 일은 초반 속도를 확실히 늦춘다. 요구사항이 아직 탐색 단계라면 표를 채우려는 시도 자체가 낭비가 될 수도 있다.
반대로 얻는 것은 두 가지다. 첫째, 기대값이 구현과 독립적이므로 구현을 통째로 다시 써도 테스트를 그대로 쓸 수 있다. 둘째, 논쟁이 생겼을 때 "코드가 이러니까"가 아니라 문서의 한 줄을 가리켜 결론을 낼 수 있다.
암시적 오라클(크래시 없음)만 쓰면 비용은 거의 없지만 도메인 오류를 전혀 못 잡고, 명세 오라클은 비용이 크지만 검출력이 가장 높다. 위험도에 따라 섞는 것이 현실적이다.
사용하면 안 되는 경우
- 프로토타입 탐색 단계. 무엇을 만들지 자체가 미정이면 판정 기준을 고정하는 것이 오히려 방해가 된다.
- 정답이 본질적으로 여러 개인 영역. 레이아웃 미세 조정, 카피 문구, 추천 순서 같은 것은 단일 기대값으로 못 박으면 정상 변경마다 테스트가 깨진다. 이런 영역은 불변식(겹침 없음, 항목 수 보존)이나 시각 회귀로 다룬다.
- 외부 시스템이 정답을 정하는 경우. 서드파티 응답을 그대로 기대값으로 굳히면 상대가 바꿀 때마다 우리 테스트가 깨진다. 계약의 형태만 검증한다.
흔한 실수
- 구현을 돌려 보고 기대값을 복사한다. 가장 흔하고 가장 조용한 실패다. 판별법은 간단하다. "구현을 지우고 다시 써도 이 기대값을 그대로 쓸 수 있는가?"
- 테스트가 빨개지면 반사적으로 테스트를 고친다. 요구사항과 구현이 어긋난 신호일 수 있으므로, 먼저 어느 쪽이 틀렸는지 확인한다.
- 판정 기준이 사람 머릿속에만 있다. 그 사람이 자리를 비우면 팀 전체가 정답을 모르는 상태가 된다.
- 미결 항목을 관례로 채운다. "보통 이렇게 하니까"로 채운 행은 나중에 요구사항 위반으로 되돌아온다.
- 오라클과 하네스를 혼동한다. locator가 잘못돼 실패한 것은 판정 기준의 문제가 아니라 도구의 문제다. 원인을 분류하지 않으면 엉뚱한 곳을 고친다.
관련 개념
- mutation-testing — 판정 기준이 실제로 작동하는지(틀린 구현을 잡는지) 역으로 검증하는 방법
- playwright-flaky-vs-failed-triage — 판정 결과 자체를 신뢰할 수 있는지 가려내는 문제