본문 바로가기

테스트 주도 개발 (TDD)

실패 테스트·최소 구현·리팩터링의 Red-Green-Refactor 순환으로 요구사항을 작은 단위의 검증 가능한 코드로 발전시킵니다.

테스트 주도 개발(Test-Driven Development, TDD)은 코드보다 테스트를 먼저 작성하는 개발 방식입니다.

실패하는 테스트를 만들고, 그 테스트를 통과할 최소한의 코드를 작성한 뒤, 리팩터링하는 사이클을 반복합니다.

즉, 테스트를 단순 검증 도구가 아니라 설계 프로세스의 핵심으로 활용하는 접근입니다.

TDD는 Kent Beck이 XP(Extreme Programming)의 일환으로 주창했으며, 코드의 품질을 높이고 개발 생산성을 향상시키는 데 큰 기여를 합니다.

타입스크립트와 같은 강력한 타입 시스템을 가진 언어에서는 TDD를 통해 런타임 오류뿐만 아니라 타입 관련 오류까지도 더욱 견고하게 방지할 수 있습니다.

작은 예제가 실행 가능한 명세를 성장시킨다 — 상태 추적

실패하는 capitalize 조건부터 구현 변경과 재검사 상태가 바뀐다. 본문에 남은 leading-space 실패를 숨기지 않고 RED/GREEN 경계를 명시해야 한다.

RED 실패 확인→GREEN 테스트 통과→REFACTOR 동작 유지→다음 요구의 RED로 순환한다. 본문 capitalize는 앞 공백 테스트가 아직 실패하는 상태임을 별도 카드로 보여준다.

RED → GREEN → REFACTOR

의미 있는 실패를 통과시킨 뒤 동작을 유지하며 구조를 고친다

관찰 가능한 예제를 고르고 RED→GREEN→REFACTOR를 반복합니다. GREEN에는 요구한 테스트 통과가 필요하고, REFACTOR는 그 동작을 유지한 구조 변경입니다.

의미 있는 실패를 통과시킨 뒤 동작을 유지하며 구조를 고친다RED 실패 확인→GREEN 테스트 통과→REFACTOR 동작 유지→다음 요구의 RED로 순환한다. 본문 capitalize는 앞 공백 테스트가 아직 실패하는 상태임을 별도 카드로 보여준다.RED올바른 이유의 실패GREEN요구 테스트 통과REFACTOR동작 유지 · 구조 정리
테스트의 의미 있는 실패에서 통과와 구조 정리로 순환한다RED에서 요구의 실패를 확인하고 GREEN에서 요구 테스트를 통과시키며 REFACTOR에서 같은 동작을 유지한다. 새 요구는 왼쪽 반환 화살표를 따라 RED에서 시작한다. 앞 공백을 처리하지 않는 원문 구현과 보완 구현을 구별한다.RED올바른 이유의 실패GREEN요구 테스트 통과REFACTOR동작 유지 · 구조 정리다음 요구는다시 RED에서 확인

RED

테스트가 요구를 관찰하는가?

설정 오류나 잘못된 기대값이 아니라 필요한 행동이 없어서 실패하는지 확인합니다. 이미 통과했다면 구현 상태와 테스트 의미부터 점검합니다.

GREEN

작은 구현으로 통과한다

새 요구와 기존 회귀 테스트를 통과시킬 구현을 작성합니다. 통과하지 않은 채 구조 정리 단계로 넘어가지 않습니다.

REFACTOR

행동 계약을 유지한다

중복·이름·구조를 정리한 뒤 같은 테스트를 다시 실행합니다. 새 동작 요구는 다음 RED의 사례로 다룹니다.

ACTUAL LESSON

capitalize는 아직 GREEN이 아니다

원문에 제시한 첫 문자 대문자화는 ' world'를 그대로 반환해 ' World' 요구에 실패합니다. trimStart만 쓰면 공백 보존 요구도 깨집니다.

EVIDENCE

타입 검사와 실행 검증

TypeScript 검사와 Jest의 런타임 기대값 검증은 다른 책임입니다. 이 그림은 교재 테스트를 실제로 실행해 모두 통과했다고 보고하는 결과가 아닙니다.

테스트는 관찰한 범위의 회귀 증거입니다. 모든 입력·통합 경로의 안전을 자동으로 보장하지 않습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 캡션RED 실패 확인→GREEN 테스트 통과→REFACTOR 동작 유지→다음 요구의 RED로 순환한다. 본문 capitalize는 앞 공백 테스트가 아직 실패하는 상태임을 별도 카드로 보여 준다.
원도표 설명원문에 제시한 첫 문자 대문자화는 ' world'를 그대로 반환해 ' World' 요구에 실패합니다. trimStart만 쓰면 공백 보존 요구도 깨집니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
실패를 관찰test: leading-space 조건 실패 / 구현: 현재 동작공백으로 시작하는 capitalize 입력의 기대 결과와 구현 결과가 다르다실패를 실제로 드러낸 조건을 먼저 고정한다
최소 수정 모델수정: 설계 보강 제안 / test: 재검사 필요기대 계약에 맞는 처리를 제안하고 같은 테스트를 다시 검사한다본문 구현이 이미 GREEN이라고 주장하지 않는다
회귀와 재구성조건: 동일 입력/기대 / 결과: 다시 통과 확인 필요GREEN을 확인한 뒤 구조를 바꾸고 같은 계약을 재검사한다REFACTOR는 기대 계약을 바꾸는 단계가 아니다

작은 예제가 실행 가능한 명세를 성장시킨다 — 상태 추적

실패하는 capitalize 조건부터 구현 변경과 재검사 상태가 바뀐다. 본문에 남은 leading-space 실패를 숨기지 않고 RED/GREEN 경계를 명시해야 한다.

RED 실패 확인→GREEN 테스트 통과→REFACTOR 동작 유지→다음 요구의 RED로 순환한다. 본문 capitalize는 앞 공백 테스트가 아직 실패하는 상태임을 별도 카드로 보여준다.

RED → GREEN → REFACTOR

의미 있는 실패를 통과시킨 뒤 동작을 유지하며 구조를 고친다

관찰 가능한 예제를 고르고 RED→GREEN→REFACTOR를 반복합니다. GREEN에는 요구한 테스트 통과가 필요하고, REFACTOR는 그 동작을 유지한 구조 변경입니다.

의미 있는 실패를 통과시킨 뒤 동작을 유지하며 구조를 고친다RED 실패 확인→GREEN 테스트 통과→REFACTOR 동작 유지→다음 요구의 RED로 순환한다. 본문 capitalize는 앞 공백 테스트가 아직 실패하는 상태임을 별도 카드로 보여준다.RED올바른 이유의 실패GREEN요구 테스트 통과REFACTOR동작 유지 · 구조 정리
테스트의 의미 있는 실패에서 통과와 구조 정리로 순환한다RED에서 요구의 실패를 확인하고 GREEN에서 요구 테스트를 통과시키며 REFACTOR에서 같은 동작을 유지한다. 새 요구는 왼쪽 반환 화살표를 따라 RED에서 시작한다. 앞 공백을 처리하지 않는 원문 구현과 보완 구현을 구별한다.RED올바른 이유의 실패GREEN요구 테스트 통과REFACTOR동작 유지 · 구조 정리다음 요구는다시 RED에서 확인

RED

테스트가 요구를 관찰하는가?

설정 오류나 잘못된 기대값이 아니라 필요한 행동이 없어서 실패하는지 확인합니다. 이미 통과했다면 구현 상태와 테스트 의미부터 점검합니다.

GREEN

작은 구현으로 통과한다

새 요구와 기존 회귀 테스트를 통과시킬 구현을 작성합니다. 통과하지 않은 채 구조 정리 단계로 넘어가지 않습니다.

REFACTOR

행동 계약을 유지한다

중복·이름·구조를 정리한 뒤 같은 테스트를 다시 실행합니다. 새 동작 요구는 다음 RED의 사례로 다룹니다.

ACTUAL LESSON

capitalize는 아직 GREEN이 아니다

원문에 제시한 첫 문자 대문자화는 ' world'를 그대로 반환해 ' World' 요구에 실패합니다. trimStart만 쓰면 공백 보존 요구도 깨집니다.

EVIDENCE

타입 검사와 실행 검증

TypeScript 검사와 Jest의 런타임 기대값 검증은 다른 책임입니다. 이 그림은 교재 테스트를 실제로 실행해 모두 통과했다고 보고하는 결과가 아닙니다.

테스트는 관찰한 범위의 회귀 증거입니다. 모든 입력·통합 경로의 안전을 자동으로 보장하지 않습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 캡션RED 실패 확인→GREEN 테스트 통과→REFACTOR 동작 유지→다음 요구의 RED로 순환한다. 본문 capitalize는 앞 공백 테스트가 아직 실패하는 상태임을 별도 카드로 보여 준다.
원도표 설명원문에 제시한 첫 문자 대문자화는 ' world'를 그대로 반환해 ' World' 요구에 실패합니다. trimStart만 쓰면 공백 보존 요구도 깨집니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
실패를 관찰test: leading-space 조건 실패 / 구현: 현재 동작공백으로 시작하는 capitalize 입력의 기대 결과와 구현 결과가 다르다실패를 실제로 드러낸 조건을 먼저 고정한다
최소 수정 모델수정: 설계 보강 제안 / test: 재검사 필요기대 계약에 맞는 처리를 제안하고 같은 테스트를 다시 검사한다본문 구현이 이미 GREEN이라고 주장하지 않는다
회귀와 재구성조건: 동일 입력/기대 / 결과: 다시 통과 확인 필요GREEN을 확인한 뒤 구조를 바꾸고 같은 계약을 재검사한다REFACTOR는 기대 계약을 바꾸는 단계가 아니다

TDD의 세 가지 황금률

TDD는 다음의 세 가지 단계를 반복하는 사이클로 이루어집니다.

Red (빨강)
  • 실패하는 테스트를 먼저 작성합니다. 현재 구현되어 있지 않거나, 의도적으로 잘못된 동작을 테스트하는 코드를 작성합니다.
  • 이 테스트는 실행했을 때 반드시 실패해야 합니다. 실패하지 않는다면, 테스트 자체가 잘못되었거나, 이미 기능이 구현되어 있는 경우입니다.
  • 이 단계에서 작성하는 테스트는 개발하려는 기능의 명세를 정의하는 역할을 합니다.
Green (초록)
  • 테스트를 통과시킬 수 있는 최소한의 코드를 작성합니다.
  • 이 단계에서는 오직 테스트를 통과시키는 것에만 집중하고, 코드의 아름다움이나 일반화는 고려하지 않습니다. 가장 빠르고 간단한 방법으로 테스트를 통과시킵니다.
  • 테스트를 실행했을 때 성공해야 합니다.
Refactor (리팩터링)
  • 코드를 개선합니다. 테스트가 모두 통과하는 상태이므로, 안심하고 코드를 리팩터링할 수 있습니다.
  • 중복 제거, 가독성 향상, 설계 개선, 성능 최적화 등을 수행합니다.
  • 리팩터링 후에도 모든 테스트가 여전히 성공하는지 확인합니다. 실패한다면 리팩터링이 잘못된 것이므로 되돌리거나 수정해야 합니다.

이 세 단계를 작은 기능 단위로 계속 반복하면서 소프트웨어를 점진적으로 개발해나갑니다.


TDD의 주요 이점

TDD는 다음과 같은 여러 가지 중요한 이점을 제공합니다.

높은 코드 품질 및 신뢰성
  • 작성한 테스트가 다루는 행동의 회귀를 빠르게 찾을 수 있습니다. 모든 코드·입력·통합 경로의 커버리지를 자동으로 보장하지는 않습니다.
  • 테스트가 없는 코드에 비해 리팩터링이나 기능 추가 시 변경에 대한 두려움을 줄여줍니다.
명확한 설계
  • 테스트를 먼저 작성하면서 코드의 외부 API(인터페이스)를 먼저 고민하게 됩니다. 이는 사용하기 쉬운 API와 응집도 높은 모듈을 설계하는 데 도움이 됩니다 (OCP, ISP, DIP와 연관).
  • 복잡한 기능을 한 번에 구현하기보다, 테스트를 통해 작은 단위로 쪼개어 구현하게 되므로 설계가 점진적으로 개선됩니다.
빠른 피드백
  • 기능 구현과 동시에 테스트를 통해 즉각적인 피드백을 받을 수 있습니다. 이는 문제점을 조기에 발견하고 해결하는 데 도움을 줍니다.
효율적인 리팩터링
  • 테스트 스위트가 일종의 안전망 역할을 하므로, 개발자는 마음 놓고 코드를 개선하고 최적화할 수 있습니다. 테스트가 관찰하는 범위의 회귀를 확인할 수 있으며, 테스트 밖의 동작도 고려해야 합니다.
문서화 역할
  • 잘 작성된 테스트 코드는 해당 기능의 작동 방식과 기대되는 동작에 대한 훌륭한 예시이자 문서 역할을 합니다.

TDD 실습 예제 (타입스크립트)

간단한 문자열 유틸리티 함수를 TDD 방식으로 개발하는 과정을 살펴보겠습니다.

여기서는 Jest 프레임워크를 사용합니다.

함수·문자열 메서드·모듈 import/export가 선수 학습 개념입니다. 아래 예제의 요구는 빈 문자열, 앞 공백 보존, 첫 비공백 영문자의 대문자화, 숫자·기호 시작 유지입니다. 먼저 기대값을 표로 적고, 실패가 모듈 부재인지 요구와 다른 반환값인지 구별하세요. 설정 오류를 고친 뒤에도 구현의 실패를 관찰해야 합니다.

설정: 아래는 이 절의 Jest 29·ts-jest 29·TypeScript 5.5 구성을 재현하는 별도 CommonJS 실습 프로젝트입니다. 버전 없는 설치로 최신 도구를 받은 뒤 오래된 설정 예제를 섞지 않습니다. 기존 프로젝트에 적용할 때는 해당 프로젝트의 모듈 형식과 변환기 버전을 먼저 확인합니다.

# 프로젝트 초기화
mkdir tdd-example && cd tdd-example
npm init -y

# TypeScript 및 Jest 설치
npm install --save-dev [email protected] [email protected] @types/[email protected] [email protected]

tsconfig.json 파일을 설정합니다.

tsconfig.json
{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  }
}

jest.config.js 파일을 설정합니다.

jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

package.json에 테스트 스크립트를 추가합니다.

package.json
{
  "name": "tdd-example",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watchAll"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/jest": "29.5.12",
    "jest": "29.7.0",
    "ts-jest": "29.1.5",
    "typescript": "5.5.3"
  }
}
TDD 사이클 시작: capitalize 함수 개발
Red (실패하는 테스트 작성)

src/utils/string.test.ts 파일 생성

src/utils/string.test.ts
import { capitalize } from './string'; // 아직 존재하지 않는 함수 임포트

describe('capitalize', () => {
  it('should capitalize the first letter of a string', () => {
    expect(capitalize('hello')).toBe('Hello');
  });

  it('should return an empty string for an empty input', () => {
    expect(capitalize('')).toBe('');
  });

  it('should handle strings with leading spaces', () => {
    expect(capitalize('  world')).toBe('  World');
  });

  it('should return the same string if the first character is not a letter', () => {
    expect(capitalize('123test')).toBe('123test');
    expect(capitalize('!abc')).toBe('!abc');
  });

  it('should handle single character strings', () => {
    expect(capitalize('a')).toBe('A');
  });
});

이제 npm test를 실행하면 아직 없는 모듈·export 때문에 실패할 수 있습니다. 이 결과로 반환값 요구를 관찰했다고 판단하지 않습니다. 모듈을 만들고 아래 임시 함수를 넣어 테스트가 실제 구현까지 도달하게 합니다.

src/utils/string.ts (Red 확인용 임시 구현)
export function capitalize(str: string): string {
  return str;
}

이제 'hello'의 실제 값은 'hello', 기대값은 'Hello'여서 실패합니다. 빈 문자열·숫자·기호 테스트 일부가 통과해도 영문자와 앞 공백 요구가 실패하므로 스위트는 Red입니다. 설정·임포트 오류와 이 행동 실패를 구별한 뒤 구현을 바꿉니다.

Green (테스트를 통과하는 최소한의 코드 작성)

src/utils/string.ts 파일 생성

src/utils/string.ts
export function capitalize(str: string): string {
  if (str.length === 0) {
    return '';
  }
  // 가장 간단하게 첫 글자만 대문자로 바꾸는 로직
  return str.charAt(0).toUpperCase() + str.slice(1);
}

npm test를 다시 실행합니다.

원문 구현은 첫 문자인 공백만 대문자화하므로 capitalize(' world')는 그대로 ' world'입니다. 따라서 앞 공백을 유지하며 W를 대문자화하라는 테스트에는 실패하고, 아직 Green이 아닙니다. trimStart()로 결과를 만들면 앞 공백이 사라지므로 그것만으로도 기대값을 충족하지 않습니다. 앞 공백을 보존한 채 첫 비공백 문자를 처리하는 구현이 필요합니다.

빠진 Green 단계 완성하기

앞의 첫 문자 구현을 기록해 둔 뒤, 작업 파일 src/utils/string.ts에서는 capitalize를 아래 별도 보완 구현으로 교체합니다. 같은 이름의 두 함수를 한 파일에 함께 선언하지 않습니다. 이 실습은 앞서 제시한 ASCII 문자열과 공백 조건을 다룹니다.

src/utils/string.ts (앞 공백 요구까지 보완)
export function capitalize(str: string): string {
  const first = str.search(/\S/);
  if (first === -1) {
    return str;
  }
  return str.slice(0, first)
    + str.charAt(first).toUpperCase()
    + str.slice(first + 1);
}

먼저 비공백 위치를 찾습니다. 빈 문자열이나 공백만 있는 문자열이면 −1이어서 입력을 그대로 반환합니다. ' world'에서는 위치 2 앞의 두 공백을 보존하고 w를 W로 바꾼 뒤 orld를 붙입니다. trimStart는 탐색과 달리 반환값에서 공백을 제거하므로 같은 계약이 아닙니다. 앞의 6개 expect 결과가 모두 일치한 뒤에 구조 정리 단계로 넘어갑니다.

이 구현으로 통과를 예상하는 것과 독자의 프로젝트에서 Jest가 실제 통과한 것은 구별합니다. npm test로 첫 파일의 전체 테스트 결과를 확인하고 npx tsc --noEmit으로 정적 검사도 따로 수행하세요. package.json의 scripts만 병합할 때 기존 devDependencies를 지우지 않습니다. 교재에 제시한 Jest 29 계열 설정과 다른 최신 버전을 설치했다면 해당 버전의 변환기 설정을 대조합니다.

Refactor (코드 개선)

앞 공백 테스트까지 통과시킨 뒤 리팩터링해야 합니다. 아래 구현은 앞서 찾은 첫 비공백 위치를 유지하면서 접두 공백·대상 문자·나머지를 이름 있는 값으로 분리합니다. 첫 위치를 다시 0으로 바꾸면 기존 공백 요구가 깨지므로 그것은 동작 보존 리팩터링이 아닙니다.

하지만 더 복잡한 함수였다면 이 단계에서 다음과 같은 작업을 할 수 있습니다.

  • 변수 이름 변경, 함수 분리
  • 중복 코드 제거
  • 가독성 향상
  • 성능 최적화 (단, 테스트 통과가 유지되는 선에서)

숫자·기호 시작은 기존 기대값처럼 유지합니다. 새로운 문자 범위나 정규화 정책이 필요하면 리팩터링에 끼워 넣지 않고 다음 Red 단계의 별도 요구로 다룹니다.

src/utils/string.ts (리팩터링 후)
export function capitalize(str: string): string {
  const first = str.search(/\S/);
  if (first === -1) {
    return str;
  }
  const prefix = str.slice(0, first);
  const firstChar = str.charAt(first);
  const rest = str.slice(first + 1);
  return prefix + firstChar.toUpperCase() + rest;
}

리팩터링 후에도 npm test를 다시 실행하여 모든 테스트가 여전히 통과하는지 확인합니다.

TDD 사이클 반복: reverseString 함수 개발
Red (실패하는 테스트 작성)

src/utils/string.test.ts에 새로운 테스트 블록 추가

src/utils/string.test.ts (reverseString 테스트 추가)
// ... capitalize tests ...

import { reverseString } from './string'; // 이제 reverseString도 임포트

describe('reverseString', () => {
  it('should reverse a given string', () => {
    expect(reverseString('hello')).toBe('olleh');
  });

  it('should return an empty string for an empty input', () => {
    expect(reverseString('')).toBe('');
  });

  it('should handle single character strings', () => {
    expect(reverseString('a')).toBe('a');
  });

  it('should handle strings with spaces', () => {
    expect(reverseString('hello world')).toBe('dlrow olleh');
  });
});

npm test 실행 시 reverseString이 정의되지 않았다는 오류 발생 (Red 상태).

Green (테스트를 통과하는 최소한의 코드 작성)src/utils/string.ts에 reverseString 함수 추가
src/utils/string.ts
// 앞의 완성된 capitalize 구현은 그대로 두고 다음 함수만 추가합니다.
export function reverseString(str: string): string {
  // 가장 간단한 방법으로 뒤집기
  return str.split('').reverse().join('');
}

제시한 ASCII 범위의 reverseString 기대값은 이 구현과 일치합니다. 다만 전체 스위트가 Green이 되려면 앞서 남은 capitalize 실패도 먼저 해결해야 합니다. 실제 실행 결과를 제시한 것은 아닙니다.

작업 파일에는 보완한 capitalize를 유지하고 reverseString만 추가합니다. 위 블록을 추가하면서 기존 capitalize를 빈 함수로 덮어쓰지 않습니다. split('')은 UTF-16 코드 단위로 나누므로 emoji·결합 문자까지 사용자가 보는 글자 단위로 뒤집는 일반 구현으로 확장해 읽지 않습니다.

Refactor (코드 개선)

reverseString 역시 현재는 간결하므로 크게 리팩터링할 부분은 없습니다.

만약 성능이 중요한 상황이라면, split().reverse().join() 방식 대신 루프를 사용하는 방식으로 변경할 수도 있습니다.

중요한 것은 변경 후에도 테스트가 모두 통과하는지 확인하는 것입니다.

다음 요구를 독립적으로 검증하기

capitalize(' \tcode'), capitalize(' '), reverseString('ab cd')의 기대값을 먼저 쓰고 테스트를 추가해 보세요. 앞 공백과 탭은 그대로 두고 첫 비공백 영문자만 바꾸며, reverseString은 앞의 ASCII 뒤집기 계약을 유지합니다. 기대값을 맞추려고 입력을 미리 trim하지 않습니다.

기대값과 실패 위치 확인

순서대로 ' \tCode', ' ', 'dc ba'입니다. 첫 값에서는 비공백 위치 2의 c만 바뀝니다. 둘째 값은 비공백 위치가 없어 그대로 반환합니다. 셋째 값에서는 가운데 공백도 한 코드 단위로 순서가 뒤집히며 유지됩니다. 원래 첫 문자 capitalize 구현은 첫 값에서 code의 c를 바꾸지 못하므로 실패하고, 앞의 보완 구현은 이 조건을 처리합니다.

테스트가 실패하면 어떤 입력의 실제 값이 기대값과 다른지 먼저 확인하고 해당 계약을 수정합니다. 구현을 바꾼 뒤 새 테스트뿐 아니라 기존 capitalize·reverseString 테스트도 다시 확인해야 회귀를 찾을 수 있습니다.

이러한 Red-Green-Refactor 사이클을 반복하면서 점진적으로 기능을 추가하고 코드를 개선해 나가는 것이 TDD의 핵심입니다.


TDD의 현실적인 적용과 도전 과제

TDD는 유용한 방법론이지만, 모든 상황에 완벽하게 적용하기는 어려울 수 있습니다.

보호할 위험에 따라 단위·통합·E2E 중 필요한 범위를 선택합니다.

  • 초기 학습 곡선: TDD에 익숙하지 않은 개발자는 처음에는 개발 속도가 느려진다고 느낄 수 있습니다.
  • 레거시 코드: 테스트가 없는 레거시 코드에 TDD를 적용하기는 매우 어렵습니다. 이 경우, 기존 코드에 대한 캐릭터라이제이션 테스트(Characterization Tests)를 먼저 작성하여 기존 동작을 파악한 후, 리팩터링을 시작하는 것이 좋습니다.
  • UI/프레젠테이션 로직: UI 컴포넌트나 프레젠테이션 로직은 단위 테스트로만 TDD를 적용하기 어려울 수 있으며, 통합 테스트나 E2E 테스트가 함께 필요합니다.
  • 복잡한 외부 의존성: 데이터베이스, 네트워크 요청 등 외부 의존성을 가진 코드에 대한 테스트는 목(Mock) 객체나 스텁(Stub)을 적절히 사용하여 의존성을 격리해야 합니다.

그럼에도 불구하고, TDD는 소프트웨어 품질과 개발 생산성을 높이는 데 매우 효과적인 접근 방식이며, 특히 핵심 비즈니스 로직과 복잡한 알고리즘을 개발할 때 그 가치가 빛을 발합니다.

타입스크립트의 강력한 타입 시스템은 TDD와 결합될 때 더욱 견고한 소프트웨어 개발을 가능하게 합니다.


이것으로 10장 객체지향 프로그래밍을 마칩니다.

객체지향 원칙, 디자인 패턴, 의존성 주입, TDD는 경계 설정, 의존성 방향, 테스트 단위를 정할 때 함께 검토하는 개념입니다.

TDD는 적용 범위를 잘 고르는 것이 중요하므로, 아래 기준으로 우선순위를 잡으면 현실적인 반복이 가능합니다.

unit, integration, E2E의 서로 다른 관찰 범위와 characterization의 범위 횡단 목적, 타입 검사 역할을 비교한다.

TYPE / RUNTIME / CONTRACT

보호할 위험을 관찰할 수 있는 테스트 범위를 선택한다

프로세스 밖 호출의 유무만으로 unit과 integration을 나누지 않습니다. 규칙·모듈 협력·실제 외부 경계·사용자 여정 중 필요한 관찰 범위를 고릅니다.

UNIT

작은 규칙·행동

capitalize처럼 입출력 규칙을 빠르게 검증합니다. 외부 의존성을 대역으로 격리할 수 있지만 대역의 성공이 실제 연결 성공을 증명하지는 않습니다.

INTEGRATION

구성 요소의 협력

모듈 간 계약, adapter와 DB/HTTP, 실제 serializer·설정의 협력을 관찰합니다. 같은 프로세스 안의 통합 테스트도 가능합니다.

E2E

중요한 사용자 여정

실제 사용자 흐름의 주요 경계를 함께 통과하는지 확인합니다. 속도·재현 비용을 고려해 보호할 중요한 여정을 선택합니다.

CHARACTERIZATION

기존 동작을 먼저 고정

레거시의 관찰된 동작을 기록해 리팩터링 회귀를 찾는 목적입니다. unit에만 속하는 별도 범주가 아니라 필요한 범위에 적용할 수 있습니다.

STATIC CHECK

타입 검사도 따로 확인

type-check는 정적 타입 관계를 검사합니다. 실제 runtime 입력·IO·사용자 여정 테스트를 대신하지 않습니다.

필요한 증거를 주는 작은 범위부터 선택하고 실제 경계의 위험이 남으면 검증을 보완합니다.

Jest의 변환과 정적 검사 역할은 29.7 공식 TypeScript 시작 안내에서 확인할 수 있습니다. 테스트 하나의 통과는 관찰한 입력의 증거이며, 실제 HTTP·DB 연결이나 모든 Unicode 문자열의 처리를 증명하지 않습니다.

실패의 원인과 회귀를 구별하는 연습

  1. 같은 테스트를 실행했을 때 A는 Cannot find module './string', B는 capitalize('hello')의 기대값 Hello와 실제 값 hello가 다르다는 결과입니다. 어떤 결과가 반환값 계약의 실패를 관찰한 것이며 A 뒤에는 무엇을 확인해야 합니까?
  2. 앞 공백을 처리한 Green 구현을 첫 문자만 처리하는 함수로 바꿨습니다. hello 테스트는 통과하고 world 테스트는 실패합니다. 이를 리팩터링 완료라고 부를 수 있습니까? 원래 계약을 유지하는 이 절의 최종 함수에서 world, 공백만 있는 문자열, 123test의 결과를 쓰세요.
Red와 리팩터링의 조건 풀이
  1. B는 테스트가 함수를 호출하고 반환값을 비교한 행동 실패입니다. A는 아직 그 비교에 도달하지 못했습니다. 파일 경로·export·변환기 설정을 고쳐 호출이 가능하게 한 뒤 임시 구현의 잘못된 반환값 때문에 실패하는지 확인합니다. A를 해결했다고 새 기능이 구현되었다는 뜻도 아닙니다.
  2. 기존 요구를 깨뜨렸으므로 동작 보존 리팩터링 완료가 아닙니다. 실패를 고치거나 동작을 보존하는 이전 구현으로 돌아가 기존·새 테스트를 함께 확인합니다. 최종 함수에서는 차례로 World, 원래 공백 그대로, 123test입니다. first === -1 조건과 접두 부분 slice(0, first)가 공백만 있는 입력과 앞 공백 보존을 담당합니다.

앞의 순환 도식에서 “아직 GREEN이 아니다”라는 설명은 첫 문자만 처리한 중간 구현을 가리킵니다. 보완 구현과 마지막 리팩터링 함수는 같은 앞 공백 계약을 유지하며, 실제 프로젝트의 전체 테스트 결과로 통과를 확인합니다.