테스트 기대값은 구현과 같은 곳에서 가져와야 할까?

발행: (2026년 2월 9일 오전 02:44 GMT+9)
15 분 소요
원문: Dev.to

Source: Dev.to

Source:

소개

다음은 동일한 동작을 검증하는 두 개의 테스트입니다:

test('calculates tax correctly', () => {
  const price = 1000;
  const tax = calculateTax(price);
  expect(tax).toBe(100);
});
import { TAX_RATE } from './config';

test('calculates tax correctly', () => {
  const price = 1000;
  const tax = calculateTax(price);
  expect(tax).toBe(price * TAX_RATE);
});

두 테스트 모두 같은 내용을 확인하지만, 기대값이 완전히 다른 곳에서 가져옵니다.
첫 번째 테스트는 100을 하드코딩하고, 두 번째 테스트는 설정 파일에서 가져옵니다. 이 차이가 중요한가요?

이를 살펴보면서 테스트 기대값이 어디서 와야 하는지에 대한 보다 근본적인 질문이 떠올랐습니다. 아직 정확한 답을 찾았다고 확신할 수는 없지만, 최소한 생각할 거리를 제공할 수 있기를 바랍니다.

테스트 오라클의 개념

1978년 William E. Howden은 테스트 오라클이라는 개념을 도입했습니다. 그는 이후 1981년 출판물에서 이 아이디어를 보다 명확히 설명했습니다:

테스트를 사용하려면 테스트 출력의 정확성을 확인할 수 있는 외부 메커니즘이 존재해야 합니다. 이 메커니즘을 테스트 오라클이라고 합니다.

여기서 외부라는 단어가 핵심인 듯합니다. 테스트에서 정확성을 판단하는 데 사용되는 정보는 프로그램 자체가 아닌 다른 무언가에서 와야 합니다.

이 아이디어는 이후 다양한 방향으로 발전되었습니다: Elaine Weyuker의 “비‑테스트 가능” 프로그램 정의, Cem Kaner가 소프트웨어 테스트 교육의 핵심 과제로 제시한 오라클 문제 강조, 그리고 James Bach와 Michael Bolton의 오라클‑일관성 휴리스틱 등등.

특히 Bolton은 오라클의 목적이 정확성을 증명하는 것이 아니라 문제를 발견하는 것이라고 주장했습니다. 이 구분은 현재 논의 중인 질문과 특히 관련이 있어 보입니다.

설정에서 가져오는 테스트

소개에서 두 번째 버전을 자세히 살펴보겠습니다 — 설정(config)에서 가져오는 버전입니다:

import { TAX_RATE } from './config';

test('calculates tax correctly', () => {
  const price = 1000;
  const tax = calculateTax(price);
  expect(tax).toBe(price * TAX_RATE);
});

테스트는 합리적으로 보입니다. DRY 원칙을 따르고 있으며, 값이 변경되면 한 곳만 업데이트하면 됩니다.

하지만 Howden의 원칙을 적용해 보면 이 테스트에 문제가 있을 수 있습니다. 기대값이 구현과 동일한 config에서 가져오기 때문입니다. 누군가 TAX_RATE를 실수로 0.01로 변경하면, 테스트는 계속 통과하게 됩니다. 테스트가 구현의 거울이 되어버렸으며, 거울은 원본과 동일한 왜곡을 반영합니다.

하드코딩이 문제를 해결할까?

이제 첫 번째 버전을 살펴보자 — 하드코딩된 100이 있는 버전:

test('calculates tax correctly', () => {
  const price = 1000;
  const tax = calculateTax(price);
  expect(tax).toBe(100); // assuming 10% tax rate
});

100을 하드코딩함으로써 기대값은 구현과 무관해진다. TAX_RATE가 실수로 변경되면 테스트가 실패하고 문제를 잡아낼 수 있다.

하지만 이 경우 다른 문제가 생긴다. 100은 하드코딩된 값이므로, TAX_RATE의도적으로 변경될 때도 테스트가 실패한다. 이는 “문제 감지”가 아니라 단순히 테스트 유지보수상의 실수이다.

게다가 100에 대한 근거가 단지 “설정 파일에 0.1이 적혀 있으니 1000 × 0.1 = 100”이라는 것이라면, 실제로는 설정 값을 수동으로 복사한 것과 같다. 이는 본질적으로 하드코딩된 상수를 중복하는 것과 동일하다.

반면에 근거가 “법률에 10 % 세율이 명시되어 있다”는 경우라면, 기대값은 구현과 무관한 외부 사실에 기반한다 — 바로 Howden이 말한 “외부 메커니즘”이다.

다시 말해, 같은 하드코딩된 100이라도 코드를 보지 않고 기대값을 설명할 수 있는지에 따라 의미가 달라진다. 이것이 중복된 하드코드와 독립적인 오라클을 구분하는 기준이라고 볼 수 있다. 더 좋은 기준이 있을 수도 있지만, 현재 내 연구에서 도출한 결론은 이와 같다.

DRY와의 관계 재고

지금까지의 이야기를 요약하면:

  • 설정 파일에서 가져오는 것은 DRY를 따릅니다.
  • 기대값을 하드코딩하는 것은 DRY를 위반하는 것으로 보입니다.

DRY를 따르는 것이 오라클 독립성을 약화시키고; 오라클 독립성을 유지하는 것이 DRY를 위반하는 것처럼 보입니다. 두 원칙이 충돌하는 것처럼 보입니다.

하지만 DRY의 원래 정의로 돌아가 보면, 이들은 전혀 충돌하지 않을 수도 있습니다.

DRY는 지식, 의도의 중복에 관한 것이다.
Andy Hunt & Dave Thomas, The Pragmatic Programmer (20th Anniversary Edition)

“모든 코드 중복이 지식 중복은 아니다”라는 섹션에서, 나이 검증과 수량 검증을 위한 코드가 동일하더라도, 그 검증들은 서로 다른 지식을 나타낸다고 설명합니다. 이는 우연일 뿐이며, DRY 위반이 아닙니다.

이를 테스트 기대값에 적용하면:

  • 프로덕션 코드 TAX_RATE = 0.1은 “이 시스템은 0.1의 세율로 동작한다”는 지식을 나타냅니다.
  • 테스트 expect(tax).toBe(100)은 “세율이 10 %일 때, 1000에 대한 세금은 100이어야 한다”는 지식을 나타냅니다.

이 두 경우는 같은 숫자 값을 사용하지만, 다른 지식을 표현합니다. 전자는 시스템 구성이며, 후자는 비즈니스 규칙의 검증입니다. The Pragmatic Programmer의 정의를 엄격히 따르면, 테스트에서 0.1(또는 100)을 하드코딩하는 것은 DRY 위반이 아닙니다 — 단지 “같은 값을 공유하는 다른 지식”일 뿐입니다.

즉, DRY와 오라클 독립성은 실제로는 충돌하지 않을 수도 있습니다.

충돌, DRY의 원래 정의로 돌아가면

DRY가 코드 중복을 제거한다는 식으로 좁게 해석될 때만 겉보이는 충돌이 발생합니다.

이 해석이 정확하다고 확신할 수는 없습니다. 하지만 원래 정의에 따르면, 하드코딩된 테스트 기대값을 DRY 위반이라고 보는 것은 다소 억지일 수 있습니다.

설정값을 공유하는 것이 위험해지는 경우

설정값 공유 문제로 돌아가 보겠습니다. 하드코딩이 정당화되는 경우라도 고려할 구조적인 문제가 있습니다.

시스템의 모든 단위가 동일한 설정 상수를 참조한다면, 그 설정에 잘못된 값이 들어갔을 때 그 값을 의존하는 모든 단위에 오류가 전파됩니다. 테스트도 같은 설정을 참조한다면 테스트조차도 그 실수를 잡아내지 못합니다. 이 위험은 시스템이 모놀리식이든 마이크로서비스 아키텍처이든 관계없이 존재합니다. 이는 단일 진실 원천에 의존하는 데서 오는 고유한 위험입니다.

단위 수준에서 생각해 보면, 각 단위의 테스트가 입력값을 “아마도 올바른” 것으로 믿고 그대로 사용한다면, 그 값에 오류가 있을 경우 테스트 계층 전체에 걸쳐 오류가 잡히지 않고 전파됩니다. 다소 강한 주장처럼 보일 수 있지만 논리적으로는 성립합니다.

정확한 값보다 속성 테스트하기

모든 설정값에 대해 독립적인 하드코딩 기대값을 준비하는 것은 현실적이지 않습니다. 세율은 법적 근거가 있지만, 재시도 횟수는 어떨까요? “이 값은 3이어야 한다”는 외부 권위가 보통 없습니다. 이런 경우 독립적인 오라클을 찾는 것이 실제로 어렵습니다.

도움이 될 수 있는 한 가지 접근법은 정확한 값이 아니라 속성 및 관계를 테스트하는 것입니다:

test('retry count is within a reasonable range', () => {
  expect(MAX_RETRY).toBeGreaterThan(0);
  expect(MAX_RETRY).toBeLessThanOrEqual(10);
});

test('retry count and timeout are consistent', () => {
  expect(MAX_RETRY * RETRY_INTERVAL).toBeLessThanOrEqual(TIMEOUT);
});

이와 같은 테스트는 값이 바뀌어도 업데이트할 필요가 없습니다. 의도치 않은 파손만을 잡아냅니다. 그리고 제약 조건이 설계 의도에 기반하므로—외부적인 정당성을 제공하므로—Howden 원칙과도 일치하는 것으로 보입니다.

결국 이것도 트레이드‑오프이다

Howden 원칙은 이론적으로 타당합니다. 모든 기대값이 구현과 독립적이어야 한다는 주장은 논리적으로 옳습니다. 하지만 모든 설정값에 대해 독립적인 검증을 적용하는 것은 현실적이지 않습니다.

가장 중요한 것은 당신이 트레이드‑오프를 인식하고 있는가 입니다. “이 값은 이상적으로는 독립적으로 검증돼야 하지만, 우리는 의식적인 비용‑편익 판단 아래 설정에서 공유한다”는 결정과, 아무 생각 없이 단순히 공유하는 것 사이에는 큰 차이가 있습니다.

원래 질문으로 돌아가면: 기대값이 하드코딩된 것이든 설정에서 가져온 것이든 중요한가? 저는 그렇다고 생각합니다—하지만 어느 한 접근법이 보편적으로 더 좋다는 이유 때문은 아닙니다. 중요한 것은 선택이 의도적이었으며, 무엇을 얻고 무엇을 잃는지 이해하고 있었는가 입니다.

아마도 테스트의 가치는 형태가 아니라 그 뒤에 있는 판단의 품질에 의해 결정될 것입니다. 제가 틀렸을 수도 있지만, 이것이 제가 이 연구를 통해 도출한 결론입니다.

References

  • William E. Howden, “프로그램 테스트에 대한 이론적 및 실증적 연구”, IEEE Transactions on Software Engineering, 1978
  • William E. Howden, “동적 분석 방법에 대한 조사”, in Software Validation and Testing Techniques, IEEE Computer Society, 1981
  • Andy Hunt & Dave Thomas, 실용주의 프로그래머: 숙련으로 가는 여정, 20주년 기념판, Addison‑Wesley, 2019
  • Earl T. Barr, Mark Harman, Phil McMinn, Muzammil Shahbaz, Shin Yoo, “소프트웨어 테스트에서의 오라클 문제: 조사”, IEEE Transactions on Software Engineering, 2015
  • The Oracle Problem – YLD Blog
  • The Oracle Problem and the Teaching of Software Testing – Cem Kaner
  • Oracles Are About Problems, Not Correctness – DevelopSense
  • The DRY Principle and Incidental Duplication – Anthony Sciamanna
Back to Blog

관련 글

더 보기 »

JavaScript용 UEFI 바인딩

UEFI 바인딩 for JavaScript https://codeberg.org/smnx/promethee Hacker News 토론 https://news.ycombinator.com/item?id=46945348 – 11점, 4댓글....

Nim

구성 파일 nim ~/.config/nim/config.nims import std/strutils, strformat switch'nimcache', fmt'{getCurrentDir}/nimcache/{projectName}/{CompileTime.toHex...

Go의 비밀스러운 삶: ‘defer’ 문

챕터 20: The Stacked Deck Ethan의 데스크탑 PC 팬이 크게 돌고 있었다. 그는 오류 메시지를 끊임없이 뿜어내는 터미널을 마치 부서진 불꽃처럼 바라보고 있었다.