본문 바로가기

테스트 주도 개발 (TDD)

실패하는 테스트를 먼저 쓰고, 통과할 만큼만 구현하고, 동작을 지킨 채 구조를 고치는 Red-Green-Refactor의 순환을 Jest로 문자열 유틸리티를 만들며 따라가고, 설정의 실패와 행동의 실패를 구분하는 방법과 상황에 맞는 테스트 범위를 고르는 기준을 다룹니다.

테스트 주도 개발(test-driven development, TDD)은 코드보다 테스트를 먼저 쓰는 개발 방식입니다. 만들려는 기능의 기대 결과를 테스트로 먼저 적고 그 테스트가 실패하는 것을 확인한 뒤, 테스트를 통과할 만큼만 코드를 쓰고, 통과한 상태를 지키면서 코드의 구조를 다듬습니다. 켄트 벡(Kent Beck)이 익스트림 프로그래밍(extreme programming)의 실천법으로 널리 알렸으며, 테스트를 검증의 도구일 뿐 아니라 설계를 이끄는 도구로 씁니다.

이 글은 TDD의 순환을 먼저 정리하고, Jest라는 테스트 도구로 문자열 함수 두 개를 그 순서대로 만들어 봅니다. 그 과정에서 테스트가 실패한 이유가 설정 때문인지 동작 때문인지 구분하는 방법을 확인하고, 마지막으로 상황에 맞는 테스트의 범위를 고르는 기준을 봅니다.

Red, Green, Refactor

TDD는 세 단계를 작은 기능 단위로 되풀이합니다. 첫 단계인 Red에서는 아직 없는 기능의 기대 결과를 테스트로 쓰고, 그 테스트가 실패하는 것을 확인합니다. 처음부터 통과한다면 테스트가 잘못되었거나 이미 그 기능이 있는 것이므로, 실패를 직접 보는 것이 이 단계의 목적입니다.

둘째 단계인 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는 기대 계약을 바꾸는 단계가 아니다

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

실패하는 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에서 어떻게 일어나는지 보여 줍니다. 앞 공백이 있는 입력의 실패를 먼저 확인하고, 그 요구를 만족하도록 구현을 고친 뒤 같은 테스트로 다시 검사하며, 통과를 확인한 다음에야 구조를 바꾸고 같은 테스트로 또 확인합니다.

이 순서를 지키면 테스트가 기능의 사용법을 먼저 정하게 되어, 쓰기 쉬운 함수의 모양을 구현보다 먼저 고민하게 됩니다. 쌓인 테스트는 나중에 코드를 고칠 때 테스트가 다루는 동작이 깨지지 않았는지 알려 주고, 기능이 어떻게 동작해야 하는지 보여 주는 예시가 되기도 합니다. 다만 테스트가 다루지 않는 입력이나 다른 모듈과의 연결까지 지켜 주지는 않습니다.

실습 준비

새 폴더 tdd-example을 만들고 다음 명령으로 프로젝트를 시작합니다. 이 실습은 Jest 30과 그 Jest에서 타입스크립트를 변환하는 ts-jest 29.4를 쓰며, ts-jest가 아직 TypeScript 7을 지원하지 않으므로 TypeScript는 이 교재의 기준인 6.0으로 고정합니다.

npm init -y
npm install --save-dev --save-exact [email protected] [email protected] @types/[email protected] [email protected]
tsconfig.json
{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "rootDir": "./src",
    "outDir": "./dist",
    "types": ["jest"],
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
jest.config.js
module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
};

jest.config.js의 preset: "ts-jest"는 테스트 파일과 소스를 ts-jest로 변환하게 하고, 타입 오류도 테스트의 실패로 보고하게 합니다. TypeScript 6.0부터 types의 기본값이 빈 배열이므로, 9장에서 본 것처럼 "types": ["jest"]를 적어야 @types/jest의 describe와 expect가 전역에 들어옵니다. 또 소스가 모두 src에 있는데 outDir만 적고 rootDir를 빼면, 6.0은 출력할 때 그 폴더를 rootDir로 직접 적으라는 TS5011 오류를 내므로 "rootDir": "./src"도 함께 적었습니다. 마지막으로 package.json의 scripts에 "test": "jest"를 더하는데, 이때 설치로 기록된 devDependencies를 지우지 않도록 scripts 항목만 고칩니다.

capitalize 만들기

만들 함수는 문자열에서 공백이 아닌 첫 글자를 대문자로 바꾸는 capitalize입니다. 요구를 테스트로 먼저 적습니다. 빈 문자열은 그대로, 앞의 공백은 유지하고, 숫자나 기호로 시작하면 바꾸지 않습니다.

src/utils/string.test.ts
import { capitalize } from "./string";

describe("capitalize", () => {
  it("첫 글자를 대문자로 바꾼다", () => {
    expect(capitalize("hello")).toBe("Hello");
    expect(capitalize("a")).toBe("A");
  });

  it("빈 문자열은 그대로 돌려준다", () => {
    expect(capitalize("")).toBe("");
  });

  it("앞의 공백은 유지하고 첫 영문자를 바꾼다", () => {
    expect(capitalize("  world")).toBe("  World");
  });

  it("숫자나 기호로 시작하면 그대로 둔다", () => {
    expect(capitalize("123test")).toBe("123test");
    expect(capitalize("!abc")).toBe("!abc");
  });
});

npm test를 실행하면 테스트는 실패하지만, 이것은 아직 기대한 실패가 아닙니다. ./string 모듈이 없으므로 ts-jest가 Cannot find module './string'이라는 타입 진단을 내고, 테스트는 capitalize를 한 번도 부르지 못한 채 멈추기 때문입니다. 행동이 틀렸다는 실패를 보려면, 먼저 함수가 불릴 수 있게 만들어야 합니다.

src/utils/string.ts
export function capitalize(str: string): string {
  return str;
}

이제 테스트가 함수를 불러 결과를 비교하므로, capitalize("hello")에서 기대한 "Hello"와 실제 "hello"가 다르다는 실패가 나옵니다. 빈 문자열과 숫자, 기호의 테스트는 우연히 통과하지만, 첫 테스트와 앞 공백 테스트가 실패하므로 전체는 Red입니다. 이것이 기대한 실패이며, 설정이나 가져오기의 실패와 행동의 실패를 이렇게 구분한 뒤에 구현으로 넘어갑니다.

Green으로 만들기

첫 글자를 대문자로 바꾸는 가장 간단한 구현을 먼저 시도해 봅니다. 빈 문자열이면 그대로 돌려주고, 아니면 첫 글자만 toUpperCase()로 바꾼 뒤 나머지를 slice(1)로 이어 붙입니다. 실패한 테스트 가운데 첫 테스트를 통과시키는 데 필요한 만큼만 적은 코드입니다.

src/utils/string.ts
export function capitalize(str: string): string {
  if (str.length === 0) {
    return "";
  }
  return str.charAt(0).toUpperCase() + str.slice(1);
}

이 구현은 "hello"를 "Hello"로 바꾸지만, " world"에서는 첫 글자인 공백을 대문자로 바꾸려 하므로 결과가 " world" 그대로입니다. 그래서 앞 공백 테스트가 여전히 실패하고, 아직 Green이 아닙니다. trimStart()로 공백을 지운 뒤 바꾸면 이번에는 앞의 공백이 사라져 "World"가 되므로, 역시 요구와 맞지 않습니다.

공백을 지우지 않고, 공백이 아닌 첫 글자의 위치를 찾아 그 글자만 바꾸어야 합니다.

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);
}

str.search(/\S/)는 공백이 아닌 첫 글자의 위치를 돌려주고, 그런 글자가 없으면 -1을 돌려줍니다. 빈 문자열이나 공백만 있는 문자열은 -1이어서 그대로 돌려주고, " world"에서는 위치 2의 w만 바꾸어 앞의 두 공백 뒤에 World를 붙입니다. npm test를 다시 실행해 네 테스트가 모두 통과하는 것을 확인하면 Green이며, 테스트 도구와 별개로 npx tsc --noEmit으로 타입 검사도 따로 확인합니다.

Refactor

통과한 상태에서 구조를 다듬습니다. 한 줄에 몰려 있던 세 부분에 이름을 붙이면, 무엇을 이어 붙이는지 읽기 쉬워집니다.

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 target = str.charAt(first);
  const rest = str.slice(first + 1);
  return prefix + target.toUpperCase() + rest;
}

다듬은 뒤에도 npm test로 네 테스트가 그대로 통과하는지 확인합니다. 리팩터링은 동작을 바꾸지 않는 변경이므로, 이 단계에서 다른 언어의 글자를 다루거나 공백을 정리하는 새 규칙을 끼워 넣지 않습니다. 그런 요구는 다음 순환의 Red에서 새 테스트로 시작합니다.

reverseString으로 한 번 더

다음 기능인 reverseString도 같은 순서로 만듭니다. 테스트 파일의 가져오기를 import { capitalize, reverseString } from "./string";으로 바꾸고, 새 테스트를 더합니다.

src/utils/string.test.ts (추가)
describe("reverseString", () => {
  it("문자열을 뒤집는다", () => {
    expect(reverseString("hello")).toBe("olleh");
    expect(reverseString("hello world")).toBe("dlrow olleh");
  });

  it("빈 문자열과 한 글자는 그대로 돌려준다", () => {
    expect(reverseString("")).toBe("");
    expect(reverseString("a")).toBe("a");
  });
});

string.ts에 reverseString이 아직 없으므로 처음에는 가져오기에서 실패하며, 앞에서처럼 빈 구현을 둔 뒤 기대값과 다르다는 실패를 확인합니다. 그다음 다음 구현을 더해 통과시킵니다.

src/utils/string.ts (추가)
export function reverseString(str: string): string {
  return str.split("").reverse().join("");
}

이 구현은 이 테스트가 다루는 영문과 공백의 입력에서 기대값과 맞습니다. 다만 split("")은 문자열을 UTF-16 코드 단위로 나누므로, 이모지나 결합 문자처럼 두 단위 이상으로 이루어진 글자는 깨질 수 있습니다. 그런 입력도 다루어야 한다면 그것을 새 요구로 정해 테스트부터 씁니다.

상황에 맞는 테스트의 범위

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·사용자 여정 테스트를 대신하지 않습니다.

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

TDD를 모든 코드에 같은 방식으로 적용할 수는 없습니다. 위 그림처럼 지키려는 위험에 따라 테스트의 범위를 고릅니다. capitalize 같은 작은 규칙은 단위 테스트(unit test)로 빠르게 확인하고, 여러 모듈이 함께 동작하는지는 통합 테스트(integration test)로, 사용자가 실제로 겪는 흐름은 E2E 테스트(end-to-end test)로 확인합니다.

테스트가 없는 예전 코드는 바로 TDD로 고치기 어렵습니다. 이때는 지금의 동작을 그대로 기록하는 특성 테스트(characterization test)를 먼저 써서 현재의 결과를 고정한 뒤, 그 테스트가 통과하는 상태에서 조금씩 고칩니다. 데이터베이스나 네트워크처럼 바깥과 연결되는 코드는 앞 글의 의존성 주입으로 대역을 넘겨 격리하되, 대역으로 통과했다고 실제 연결도 성공한다는 뜻은 아니라는 점을 기억합니다. Jest에서 타입스크립트를 쓰는 방법은 Jest 공식 안내에 있습니다.

정리

  • TDD는 실패하는 테스트를 먼저 쓰는 Red, 통과할 만큼만 구현하는 Green, 동작을 지키며 구조를 다듬는 Refactor를 작은 단위로 되풀이합니다.
  • Red에서는 모듈을 찾지 못하는 것 같은 설정의 실패와, 함수가 불려 기대값과 다른 행동의 실패를 구분하고, 뒤의 실패를 확인한 뒤 구현합니다.
  • Green은 요구한 테스트가 모두 통과한 상태이며, 일부만 통과한 구현은 아직 Green이 아닙니다.
  • Refactor는 동작을 바꾸지 않는 변경이므로, 새 규칙은 다음 순환의 Red에서 새 테스트로 시작합니다.
  • 테스트의 범위는 지키려는 위험에 맞추어 단위와 통합, E2E 가운데 고르고, 예전 코드는 특성 테스트로 현재의 동작부터 고정합니다.

연습 문제

1. 다음 요구의 기대값

capitalize(" \tcode"), capitalize(" "), reverseString("ab cd")의 기대값을 먼저 적고 테스트로 더해 보세요. 처음 시도했던 첫 글자 구현이라면 어느 테스트가 실패하나요?

해설 보기

기대값은 차례로 " \tCode", " ", "dc ba"입니다. 첫 값은 공백과 탭 뒤 위치 2의 c만 바뀌고, 둘째 값은 공백이 아닌 글자가 없어 그대로이며, 셋째 값은 가운데 공백도 한 글자로서 순서가 뒤집힙니다. 첫 글자 구현은 첫 값에서 공백을 바꾸려다 c를 바꾸지 못해 실패하고, 마지막 구현은 세 값을 모두 처리합니다.

2. 두 가지 실패

같은 테스트를 실행했을 때 A는 Cannot find module './string'을, B는 capitalize("hello")의 기대값 Hello와 실제 값 hello가 다르다는 결과를 냈습니다. 어느 쪽이 행동의 실패를 확인한 것인가요? A를 본 뒤에는 무엇을 해야 하나요?

해설 보기

B는 테스트가 함수를 불러 결과를 비교한 행동의 실패입니다. A는 모듈을 찾지 못해 비교까지 가지 못했으므로, 파일의 경로와 내보내기, 변환 설정을 고쳐 함수가 불리게 만든 뒤 B와 같은 행동의 실패를 다시 확인합니다. A를 고쳤다고 해서 기능이 구현된 것은 아닙니다.

3. 리팩터링이라 부를 수 있는가

통과한 capitalize를 첫 글자만 바꾸는 짧은 구현으로 "정리"했더니 "hello" 테스트는 통과하고 " world" 테스트는 실패했습니다. 이 변경을 리팩터링이라 부를 수 있나요?

해설 보기

부를 수 없습니다. 리팩터링은 동작을 바꾸지 않는 구조의 변경인데, 이 변경은 앞 공백을 유지한다는 기존 요구를 깨뜨렸기 때문입니다. 변경을 되돌리거나 실패를 고쳐 모든 테스트를 다시 통과시켜야 하며, 이렇게 깨진 동작을 바로 알려 주는 것이 테스트를 쌓아 두는 이유입니다.

이것으로 10장의 객체지향을 마칩니다. 다음 장의 Promise와 async/await에서는 기다려야 하는 작업을 다루는 비동기 코드를 타입과 함께 정리합니다.