다음은 경계값 누락을 설명하기 위한 예시입니다. 결제 재시도 지연을 두 배씩 늘리고 상한을 두는 함수에 테스트를 여러 개 추가했지만, 구현이 반환한 값만 스냅샷으로 저장해 음수와 상한 경계를 묻지 못했습니다. 많은 테스트가 같은 잘못된 가정을 반복한 셈입니다.

핵심 에이전트에게 “테스트를 많이 써 달라”고 하지 말고 입력 구간과 실패 위험을 먼저 표로 만들게 해야 합니다. 새 테스트가 수정 전 실패하고 수정 후 통과하는지 확인하며, 시간·네트워크 같은 의존성은 경계에서만 대체합니다.

테스트 코드보다 위험표를 먼저 요청합니다

함수명과 기대값은 예시이며 실제 제품 규칙으로 교체해야 합니다.

단위 테스트 설계의 첫 프롬프트
claude --permission-mode plan

재시도 지연 함수의 테스트 표만 작성하세요. 구현은 수정하지 마세요.
읽기: CLAUDE.md, 함수 계약, 기존 테스트, 테스트 설정
허용: Read, Grep, 현재 테스트 목록 조회
금지: 구현 수정, 스냅샷 갱신, 네트워크
반환: 정상·경계·오류 입력 / 막는 위험 / 예상 실패 / 실행 명령
중단: 제품 문서와 기존 코드의 경계 의미가 다를 때

제품 규칙을 표로 확정한 뒤 실패를 먼저 봅니다

제품 담당자가 승인한 입력표만 테스트 코드로 옮깁니다. 수정 전 실행에서 기대한 이유로 실패하는지 확인한 뒤 최소 구현 변경으로 넘어갑니다.

리뷰어는 새 테스트, 기존 테스트, 타입 검사와 변경 차이를 확인합니다. 테스트가 처음부터 통과하면 회귀를 재현하지 못한 것이고, 요구사항이 두 뜻으로 읽히면 기대값부터 다시 정해야 합니다.

많은 스냅샷도 계약을 묻지 못할 수 있습니다

예시의 기존 테스트가 여러 재시도 결과를 스냅샷으로 남겼다고 합시다. 구현이 `1000 * 2 ** attempt`를 그대로 반환해 상한을 넘겨도 스냅샷은 그 값을 승인할 수 있습니다. “최대 30초”라는 사용자 규칙이 단언에 없기 때문입니다.

좋은 단위 테스트는 구현 모양보다 깨지면 안 되는 계약을 확인합니다. 이 함수의 위험은 네 가지입니다. 0회가 1초인지, 4회가 16초인지, 5회부터 30초에 머무는지, 음수와 비정수를 거부하는지입니다.

먼저 네 구간의 표를 반환하게 합니다

첫 산출물은 코드가 아니라 입력 구간과 기대 결과 표입니다. 제품 규칙과 맞는지 확인한 표를 테스트로 옮기면 현재 구현의 실수를 기대값으로 복사할 가능성이 줄어듭니다.

테스트 코드보다 먼저 합의할 입력 구간
규칙: attempt 0은 1000ms, 이후 두 배, 최대 30000ms
유효 입력: 0 이상의 정수

표에 포함:
- 시작: 0 → 1000
- 상한 직전: 4 → 16000
- 상한 도달: 5 → 30000
- 상한 이후: 12 → 30000
- 잘못된 입력: -1, 1.5 → 예외

구현은 아직 수정하지 마세요.

실패가 먼저 보여야 회귀 테스트가 됩니다

Jest의 `test.each`로 같은 규칙의 여러 입력을 읽기 좋게 표현합니다. 새 테스트만 실행해 수정 전 `attempt 5`가 32000으로 실패하는지 확인합니다. 처음부터 통과한다면 이미 고쳐졌거나 테스트가 문제 경로를 지나지 않는 것이므로 이유를 확인합니다.

경계에서 계약을 직접 묻는 Jest 예시
import {describe, expect, test} from '@jest/globals';
import {calculateRetryDelay} from './retry';

describe('calculateRetryDelay', () => {
  test.each([[0, 1000], [4, 16000], [5, 30000], [12, 30000]])(
    '%i회 재시도는 %ims', (attempt, expected) => {
      expect(calculateRetryDelay(attempt)).toBe(expected);
    }
  );
  test.each([-1, 1.5])('%p는 거부', attempt => {
    expect(() => calculateRetryDelay(attempt)).toThrow(RangeError);
  });
});

최소 수정 뒤 결과를 계약으로 닫습니다

예상 결과는 상한·음수·비정수 테스트가 수정 전 실패하고, 입력 검증과 `Math.min(30000, 계산값)`을 추가한 뒤 새 경계 테스트와 기존 회귀가 모두 통과하는 것입니다. 구현 결과를 되풀이하던 스냅샷은 걷어내고 계약이 갈라지는 경계 사례를 남깁니다.

이 결과는 함수 규칙을 검증하지만 실제 재시도 타이머가 30초 뒤 동작하는지는 말하지 않습니다. 가짜 타이머를 쓰는 통합 테스트나 큐 동작 검증은 별도 계층의 일입니다. 단위 테스트 한 편에 네트워크와 실제 시간을 끌어오면 느리고 흔들리는 테스트가 됩니다.

Mock은 호출 횟수보다 경계의 결과를 지킵니다

결제 API를 대체해야 한다면 내부 헬퍼가 정확히 두 번 호출됐는지보다, 두 번 실패 뒤 사용자에게 재시도 가능 상태가 반환되는지 확인합니다. 리팩터링으로 내부 호출이 합쳐져도 사용자 계약은 같을 수 있기 때문입니다. 대체 객체는 네트워크 경계에만 두고 계산 함수 자체는 실제 구현을 실행합니다.

TypeScript를 Babel로 변환해 Jest를 실행하는 구성은 타입 검사를 수행하지 않을 수 있습니다. 공식 Jest 문서도 이 경우 `tsc`를 별도로 실행하거나 적절한 변환 도구를 쓰라고 안내합니다. 테스트 통과 보고에 타입 검사 결과를 섞어 쓰지 않습니다.

중단 기준은 요구사항이 둘로 읽히는 순간입니다

“첫 실패 뒤 1초”인지 “첫 재시도 시도 자체가 0회”인지 팀 문서가 서로 다르면 에이전트가 임의로 정하게 두지 않습니다. 돈, 시간, 반올림처럼 경계 의미가 제품 결정인 경우에는 기대값을 승인받을 때까지 구현을 멈춥니다.

2026년 7월 29일 Jest 공식 문서의 `test.each`, Matcher와 TypeScript 주의를 기준으로 확인했습니다. 프레임워크 문법보다 중요한 산출물은 수정 전 실패 로그, 수정 후 통과 로그, 그리고 아직 검증하지 않은 계층의 목록입니다.

공식 출처