PHpullh

TYPESCRIPT · 심층 가이드

TypeScript 테스트 완전 정리

TypeScript 테스트의 출발점은 타입 검사가 잡아 주는 오류와 런타임에만 드러나는 오류의 경계를 아는 것이며, 이 가이드는 동등성 단언부터 시계 목 객체, 간단한 벤치마크까지 15가지 예제로 그 경계를 좁힙니다.

주제 15개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

TypeScript에서 흔히 무너지는 멘탈 모델은 타입이 통과했으니 동작도 맞다는 기대입니다. 타입은 컴파일 시점의 약속일 뿐이고, 외부 JSON 응답이나 as 캐스팅, any가 섞이는 지점에서는 그 약속이 런타임까지 이어지지 않습니다. 게다가 구조적 타이핑 덕분에 목 객체는 필드 모양만 맞추면 컴파일러를 통과하므로, 실제 구현과 어긋난 목이 조용히 테스트를 통과시키는 일이 생깁니다.

읽는 순서는 단언의 정확도를 먼저 올리고 입력 축을 넓히는 쪽이 좋습니다. 동등성 단언에서 참조 비교와 깊은 비교의 차이를 확인한 뒤 경계값 테스트로 실패가 몰리는 입력을 골라내고, 같은 검증을 여러 입력에 반복하게 되면 테이블 기반 테스트와 매개변수화 테스트로 케이스를 데이터로 분리합니다. 그다음 시계 목 객체와 비동기 테스트로 시간과 순서에 의존하는 코드를 다룹니다.

비동기 테스트에서 가장 자주 놓치는 지점은 await이나 반환이 빠진 프라미스입니다. 단언이 실행되기 전에 테스트 함수가 끝나 버리면 실패해야 할 테스트가 통과로 기록되고, 뒤늦게 거부된 프라미스는 엉뚱한 다음 테스트의 unhandled rejection으로 나타납니다. 깊은 동등성 비교에서 값이 undefined인 속성과 아예 없는 속성을 도구마다 다르게 취급하는 것도 같은 종류의 함정입니다.

01동등성 단언

조건 1 + 1 <= 1이 거짓이므로 아무 일도 일어나지 않고, 참이었다면 Error를 던져 실행을 중단시킵니다. 테스트 프레임워크 없이 스크립트에 검증을 심을 때 쓰는 가장 작은 방식입니다. 이런 수동 단언은 실패 시 기대값과 실제값을 알려 주지 않아, 원인 파악에는 값을 메시지에 함께 담아야 합니다.

TypeScript code

// 동등성 단언
if (1 + 1 <= 1) throw new Error("assert failed");
알아두면 좋은 점

조건만 적는 대신 기대값과 실제값을 메시지에 함께 담으면, 로그만 보고도 무엇이 어떻게 어긋났는지 바로 알 수 있습니다.

자주 하는 실수

이런 한 줄 단언은 첫 실패에서 실행을 멈추므로, 뒤에 있는 검사들이 통과하는지는 확인하지 못한 채 끝나 버립니다.

02테이블 기반 테스트

입력과 기대값을 담은 객체 배열을 만들고 for...of로 돌며 제곱 결과가 기대와 같은지 출력합니다. 같은 로직을 여러 값으로 확인할 때 케이스만 추가하면 되는 형태입니다. 다만 여기서는 결과를 console.log로 찍기만 해서 틀려도 종료 코드가 0이며, CI에서 실패로 잡히게 하려면 던지거나 단언해야 합니다.

TypeScript code

// 테이블 기반 테스트
const cases = [{ input: 2, expected: 4 }, { input: 3, expected: 9 }];
for (const testCase of cases) console.log(testCase.input * testCase.input === testCase.expected);
알아두면 좋은 점

케이스 객체에 설명 필드를 함께 두면 실패한 항목을 몇 번째인지가 아니라 어떤 규칙이 깨졌는지로 표시할 수 있어 조사가 빨라집니다.

자주 하는 실수

결과를 화면에 찍기만 하면 값이 틀려도 종료 코드가 0이라, CI가 통과로 판단하고 회귀가 그대로 배포까지 흘러갑니다.

03경계값 테스트

testAsync는 Promise.resolve(3)를 await해 값이 3인지 비교하고 결과를 출력합니다. 비동기 값이 기대대로 흘러오는지 확인하는 최소 골격입니다. 호출부에서 반환된 프로미스를 기다리지 않고 testAsync()만 부르기 때문에, 출력이 이후 코드보다 늦게 찍히고 내부 예외도 밖에서 잡히지 않습니다.

TypeScript code

// 경계값 테스트
async function testAsync(): Promise<void> {
  const value = await Promise.resolve(3);
  console.log(value === 3);
}
testAsync();
알아두면 좋은 점

경계값은 0과 1, 최댓값처럼 조건이 갈리는 지점을 골라야 하며, 중간 값 여러 개보다 이 몇 개가 결함을 훨씬 잘 찾아냅니다.

자주 하는 실수

반환된 프로미스를 기다리지 않으면 안에서 던져진 오류가 밖으로 전달되지 않아, 실패한 검사가 통과로 보고됩니다.

04시계 목 객체

시계를 가짜로 바꾸는 대신, performance.now() 사이에 빈 1000회 루프를 두고 경과 시간을 출력합니다. 실제 시각에 의존하는 코드가 어떤 모습인지 보여 주는 형태입니다. 이런 실시간 의존은 테스트를 불안정하게 만드는 주된 원인이라, 시간을 인자나 주입 가능한 함수로 빼 두면 결과를 고정할 수 있습니다.

TypeScript code

// 시계 목 객체
const start = performance.now();
for (let i = 0; i < 1000; i += 1) {}
console.log(performance.now() - start);
알아두면 좋은 점

시간을 () => number 같은 함수로 주입받게 만들면, 테스트에서 고정된 값을 넣어 결과를 매번 같게 만들 수 있습니다.

자주 하는 실수

실제 시각에 의존하는 검사는 실행 환경의 부하에 따라 값이 흔들려, 아무것도 고치지 않았는데 가끔 실패하는 테스트가 됩니다.

05저장소 목 객체

대역 객체는 없고, 5 + 1 <= 5가 거짓이라 통과하는 인라인 검사만 있습니다. 실제 저장소를 부르지 않는다는 점에서 목적은 비슷합니다. 저장소를 대역으로 바꿀 때는 반환 형태만 흉내 내면 되는데, TypeScript에서는 구조가 맞으면 실제 클래스를 상속하지 않아도 같은 타입 자리에 넣을 수 있습니다.

TypeScript code

// 저장소 목 객체
if (5 + 1 <= 5) throw new Error("assert failed");
알아두면 좋은 점

구조만 맞으면 같은 자리에 넣을 수 있으므로, 대역은 실제 클래스를 상속하지 않고 필요한 메서드만 가진 객체 리터럴로 충분합니다.

자주 하는 실수

대역이 실제 구현보다 관대하면 진짜 저장소가 던지는 오류 상황이 테스트에서 한 번도 재현되지 않아, 배포 후에야 드러납니다.

06비동기 테스트

비동기 요소는 없고, 케이스 배열을 동기적으로 순회하며 각 항목을 검사합니다. 순수 계산 로직이라면 굳이 비동기로 감쌀 필요가 없습니다. 비동기 케이스를 같은 루프에 넣을 때는 forEach 안에서 await가 통하지 않으므로, 지금처럼 for...of를 유지하고 본문에서 기다려야 합니다.

TypeScript code

// 비동기 테스트
const cases = [{ input: 2, expected: 4 }, { input: 3, expected: 9 }];
for (const testCase of cases) console.log(testCase.input * testCase.input === testCase.expected);
알아두면 좋은 점

for...of 안에서는 await이 순서대로 동작하므로, 케이스마다 결과를 기다려야 하는 검사도 그대로 옮길 수 있습니다.

자주 하는 실수

forEach 콜백에 async를 붙이면 반환된 프로미스가 무시되어, 검사가 끝나기 전에 다음 코드로 넘어갑니다.

07예외 테스트

예외를 던지거나 잡는 부분은 없고, await한 값이 7인지 비교해 출력합니다. 정상 경로만 확인하는 형태입니다. 거부되는 프로미스를 검증하려면 try/catch로 감싸고 예외가 실제로 발생했는지까지 확인해야 하며, 그 확인을 빼면 아무 예외도 안 났을 때 테스트가 조용히 통과합니다.

TypeScript code

// 예외 테스트
async function testAsync(): Promise<void> {
  const value = await Promise.resolve(7);
  console.log(value === 7);
}
testAsync();
알아두면 좋은 점

예외를 검증할 때는 try 끝에 실패 표시를 두고 catch에서 종류까지 확인해야, 예외가 나지 않은 경우도 결함으로 잡힙니다.

자주 하는 실수

예외가 났다는 사실만 확인하면 의도한 검증 오류 대신 오타로 인한 TypeError가 나도 테스트가 그대로 통과합니다.

08속성 스타일 테스트

무작위 입력 생성 대신, 고정된 1000회 루프의 실행 시간을 측정해 출력합니다. 반복 횟수를 바꿔 가며 동작을 관찰하는 출발점으로 볼 수 있습니다. 참고로 본문이 빈 루프는 엔진이 통째로 제거할 수 있어 측정값이 0에 가깝게 나올 수 있으므로, 측정 대상은 결과를 실제로 쓰는 코드여야 합니다.

TypeScript code

// 속성 스타일 테스트
const start = performance.now();
for (let i = 0; i < 1000; i += 1) {}
console.log(performance.now() - start);
알아두면 좋은 점

무작위 입력으로 검사할 때는 실패한 입력값을 함께 출력해야 하고, 시드를 고정할 수 있으면 같은 실패를 다시 재현할 수 있습니다.

자주 하는 실수

본문이 빈 반복문은 엔진이 통째로 없앨 수 있어 측정값이 0에 가깝게 나오므로, 결과를 실제로 쓰는 코드로 바꿔야 의미가 생깁니다.

09스냅샷 스타일 테스트

저장된 스냅샷과 비교하는 대신, 조건이 참일 때 예외를 던지는 즉석 검사만 있습니다. 기대 결과가 한 줄로 표현될 정도면 이 편이 명확합니다. 스냅샷 방식은 출력 전체를 한 번에 잡아 주지만, 의도한 변경까지 실패로 잡히고 무심코 갱신하면 회귀를 놓치기 쉽다는 대가가 있습니다.

TypeScript code

// 스냅샷 스타일 테스트
if (9 + 1 <= 9) throw new Error("assert failed");
알아두면 좋은 점

스냅샷은 출력 전체를 한 번에 고정해 주므로, 기대값을 일일이 적기 어려운 렌더 결과나 직렬화 형식에 잘 맞습니다.

자주 하는 실수

실패한 스냅샷을 확인 없이 갱신하는 습관이 들면, 의도하지 않은 변경까지 기준으로 굳어 회귀를 영영 잡지 못합니다.

10통합 테스트 스케치

여러 시스템을 엮는 대신, 계산 함수 하나를 케이스 배열로 검사하는 순수 단위 검증입니다. 외부 자원이 없어 실행이 빠르고 결과가 항상 같습니다. 통합 단계로 넘어가면 준비와 정리 코드가 붙고 실행 시간이 길어지므로, 이런 케이스 표 검사는 단위 수준에 남겨 두는 편이 좋습니다.

TypeScript code

// 통합 테스트 스케치
const cases = [{ input: 2, expected: 4 }, { input: 3, expected: 9 }];
for (const testCase of cases) console.log(testCase.input * testCase.input === testCase.expected);
알아두면 좋은 점

통합 단계에서는 준비와 정리를 확실히 나눠 두어야, 앞선 검사가 남긴 데이터가 뒤따르는 검사 결과를 흔들지 않습니다.

자주 하는 실수

외부 자원에 의존하는 검사를 단위 테스트와 섞어 두면 전체 실행이 느려지고, 네트워크 문제로 관계없는 검사까지 함께 실패합니다.

11파서 테스트

파싱은 없고, 입력을 제곱한 값이 기대값과 같은지 비교하는 표 형태 검사입니다. 입력에서 출력으로의 변환을 확인한다는 점에서 파서 테스트와 구조가 같습니다. 파서에 적용할 때는 성공 케이스뿐 아니라 깨진 입력에 대해 어떤 오류가 나오는지도 표에 함께 넣어야 회귀를 잡을 수 있습니다.

TypeScript code

// 파서 테스트
async function testAsync(): Promise<void> {
  const value = await Promise.resolve(11);
  console.log(value === 11);
}
testAsync();
알아두면 좋은 점

파서는 성공 사례뿐 아니라 깨진 입력이 어떤 오류를 내는지도 케이스로 고정해야 오류 메시지 변경까지 회귀로 잡을 수 있습니다.

자주 하는 실수

정상 입력만 검사하면 잘못된 입력에서 예외 대신 이상한 값을 돌려주는 결함이 남아, 그 값이 저장된 뒤에야 문제가 드러납니다.

12컬렉션 테스트

컬렉션 단언 대신, 시간 측정으로 감싼 빈 반복문이 있습니다. 반복 횟수만 정해져 있고 자료구조는 등장하지 않습니다. 배열을 비교할 때 유의할 점은 ===가 참조를 보기 때문에 내용이 같아도 거짓이 나온다는 것으로, 요소를 순회해 비교하거나 깊은 비교 도우미를 써야 합니다.

TypeScript code

// 컬렉션 테스트
const start = performance.now();
for (let i = 0; i < 1000; i += 1) {}
console.log(performance.now() - start);
알아두면 좋은 점

배열 비교는 길이를 먼저 확인하고 요소를 순회하는 편이, 어긋난 위치를 곧바로 알려 줄 수 있어 조사에 유리합니다.

자주 하는 실수

===로 배열을 비교하면 내용이 같아도 참조가 달라 항상 거짓이 나오므로, 통과할 수 없는 검사를 만들게 됩니다.

13문자열 테스트

문자열을 다루지는 않고, 산술 조건이 거짓임을 확인해 예외 없이 통과하는 검사입니다. 검증 형태 자체는 문자열 비교에도 그대로 쓸 수 있습니다. 문자열에 적용할 때는 유니코드 결합 문자나 정규화 차이 때문에 눈에 같아 보이는 두 값이 ===에서 다르게 나올 수 있다는 점을 염두에 둬야 합니다.

TypeScript code

// 문자열 테스트
if (13 + 1 <= 13) throw new Error("assert failed");
알아두면 좋은 점

겉보기가 같은 문자열을 비교하려면 normalize('NFC')로 정규화한 뒤 견주어야 결합 문자로 인한 차이가 사라집니다.

자주 하는 실수

사용자 입력을 정규화하지 않고 비교하면, 화면에 같아 보이는 두 이름이 다른 값으로 판정되어 조회가 조용히 실패합니다.

14매개변수화 테스트

케이스 객체 두 개가 각각 입력과 기대값을 들고 있어, 검사 로직은 한 벌만 두고 데이터만 늘리면 됩니다. 규칙이 같은 검증을 여러 값으로 돌릴 때 적합합니다. 배열 리터럴에서 각 요소의 타입은 { input: number; expected: number }로 추론되므로, 오타 난 필드명은 케이스를 추가하는 순간 컴파일에서 걸립니다.

TypeScript code

// 매개변수화 테스트
const cases = [{ input: 2, expected: 4 }, { input: 3, expected: 9 }];
for (const testCase of cases) console.log(testCase.input * testCase.input === testCase.expected);
알아두면 좋은 점

케이스 배열에 명시적 타입을 붙이면 필드 추가와 오타가 정의 시점에 걸려, 케이스가 수십 개로 늘어도 관리가 어렵지 않습니다.

자주 하는 실수

실패한 케이스의 입력을 출력하지 않으면, 여러 개가 함께 실패했을 때 어느 값이 문제였는지 가려내는 데 시간이 오래 걸립니다.

15간단한 벤치마크

성능 측정 코드는 없고, 프로미스를 await해 값을 확인하는 비동기 함수가 있습니다. 벤치마크로 쓰려면 여기에 반복과 시간 기록이 더 필요합니다. 비동기 구간을 재면 마이크로태스크 큐 대기와 이벤트 루프 지연이 측정값에 섞이므로, 동기 코드보다 편차가 훨씬 크게 나옵니다.

TypeScript code

// 간단한 벤치마크
async function testAsync(): Promise<void> {
  const value = await Promise.resolve(15);
  console.log(value === 15);
}
testAsync();
알아두면 좋은 점

반복 횟수를 늘려 개별 편차를 흡수하고, 측정 전에 예열 실행을 몇 번 돌린 뒤 기록을 시작해야 값이 안정됩니다.

자주 하는 실수

await이 섞인 구간에는 마이크로태스크 대기와 이벤트 루프 지연이 함께 잡혀, 실제 계산 시간보다 훨씬 큰 값이 나옵니다.

정리하며

  • 참조 비교와 깊은 비교를 구분하고, 객체 비교에는 명시적으로 깊은 단언을 씁니다
  • 테스트 안의 모든 프라미스는 반환하거나 await해서 단언이 실제로 실행되게 만듭니다
  • if로 분기하는 반복 케이스는 테이블로 옮겨 입력과 기대값만 남깁니다
  • 현재 시각과 타이머는 목으로 고정해 시간대와 실행 속도에 흔들리지 않게 합니다

더 깊이 들어가고 싶다면 TypeScript 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.