PHpullh

JAVASCRIPT · 심층 가이드

JavaScript 테스트 완전 정리

테이블 기반 테스트와 경계값, 시계 목 객체, 비동기 테스트까지 15편을 통해 무엇을 어디까지 검증해야 하는지 스스로 판단하는 기준을 세웁니다.

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

테스트를 어렵게 만드는 것은 단언문이 아니라 숨은 의존성입니다. JavaScript는 모듈 최상단에서 Date.now()를 읽거나 전역 fetch를 직접 부르기가 너무 쉬워서, 코드가 자기도 모르게 시계와 네트워크에 묶입니다. 테스트 가능한 설계란 대단한 아키텍처가 아니라 그런 값을 인자로 받게 만드는 작은 습관에 가깝습니다. 이 묶음의 목 객체 예제들도 결국 그 습관을 코드로 보여 줍니다.

동등성 단언에서 시작해 테이블 기반 테스트로 넘어가면 케이스가 늘어도 코드가 늘지 않습니다. 경계값 테스트와 매개변수화 테스트는 그 테이블을 채우는 방법이고, 시계 목 객체와 저장소 목 객체는 외부 의존을 끊어 내는 두 축입니다. 비동기 테스트와 예외 테스트는 비동기 편에서 만든 코드를 검증할 때 바로 필요해집니다.

비동기 테스트에서 가장 흔한 함정은 조용히 통과하는 테스트입니다. await을 빠뜨리면 프라미스가 완료되기 전에 테스트 함수가 끝나 버려서 실패한 단언이 무시되고 초록불이 뜹니다. 예외를 검증할 때 expect(fn()).rejects 대신 이미 호출해 버린 값을 넘기는 실수도 같은 계열입니다. 그리고 가짜 타이머를 켠 테스트는 반드시 원상복구해야 합니다. 되돌리지 않으면 그 다음 테스트가 영문 모를 이유로 멈춥니다.

01동등성 단언

조건이 어긋나면 예외를 던지는, if 한 줄짜리 수동 단언입니다. 테스트 러너 없이도 스크립트가 기대와 다르게 동작하는 순간 즉시 멈추게 만들 수 있습니다. 다만 실패 메시지에 실제 값이 담기지 않아, 어떤 값이 나왔는지 알려면 로그를 다시 심어야 합니다.

JavaScript code

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

node:assert의 strictEqual은 실패 메시지에 기대값과 실제값을 함께 담아 주어, 직접 던지는 것보다 원인을 훨씬 빨리 좁힐 수 있습니다.

자주 하는 실수

단언 메시지에 실제 값을 넣지 않으면 실패했을 때 로그를 다시 심고 재현해야 합니다. 재현이 어려운 환경에서는 그 기회를 아예 놓칩니다.

02테이블 기반 테스트

입력과 기댓값을 담은 객체 배열을 순회하며 제곱 결과가 맞는지 불리언으로 찍습니다. 같은 로직을 여러 입력으로 확인할 때 케이스만 배열에 추가하면 되어 손이 덜 갑니다. 다만 console.log만 하고 던지지 않아 결과가 틀려도 종료 코드가 0이라, CI에서는 통과로 처리됩니다.

JavaScript 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경계값 테스트

이미 이행된 Promise를 await한 뒤 값이 기대와 같은지 비교하는 비동기 함수입니다. 경계값을 다루려면 여기에 0, 최댓값, 빈 입력 같은 극단 케이스를 같은 형태로 덧붙이면 됩니다. 호출부가 await 없이 이 함수를 부르고 있어, 뒤에 코드가 있다면 비교보다 먼저 실행됩니다.

JavaScript code

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

경계값은 최솟값과 최댓값뿐 아니라 그 바로 앞뒤 값까지 함께 넣어야, 부등호를 하나 잘못 쓴 실수가 테스트에서 드러납니다.

자주 하는 실수

testAsync()를 기다리지 않고 부르면 검사 결과가 나오기 전에 프로그램이 끝날 수 있어, 실패가 출력되지 않은 채 통과한 것처럼 보입니다.

04시계 목 객체

performance.now()로 빈 반복문 1000회의 실제 경과 시간을 잽니다. 시간에 의존하는 코드를 검사할 때는 이런 진짜 시계 대신 값을 마음대로 정할 수 있는 가짜 시계를 끼워 넣습니다. 실제 시계를 그대로 쓰면 머신 부하에 따라 결과가 흔들려 이따금 실패하는 테스트가 만들어집니다.

JavaScript code

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

시간에 의존하는 코드는 현재 시각을 인자나 주입 대상으로 받게 만들면, 테스트에서 원하는 시각을 그대로 넣어 결과를 고정할 수 있습니다.

자주 하는 실수

실제 시계로 지연을 재는 테스트는 머신 부하에 따라 이따금 실패합니다. 그런 테스트를 재실행으로 넘기다 보면 진짜 회귀까지 함께 묻힙니다.

05저장소 목 객체

항상 거짓인 조건을 검사하므로 통과하면 아무 일도 일어나지 않는 형태입니다. 저장소를 흉내 내려면 이 자리에 미리 정한 데이터를 돌려주는 객체를 두고, 그 반환값을 같은 방식으로 단언하면 됩니다. 가짜 저장소는 실제 제약 조건을 검사하지 않으므로 이 테스트가 통과해도 스키마나 유니크 위반은 잡히지 않습니다.

JavaScript code

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

가짜 저장소가 호출 인자를 기록해 두면 반환값뿐 아니라 어떤 질의가 어떤 순서로 나갔는지까지 검증할 수 있습니다.

자주 하는 실수

가짜 저장소는 유니크 제약이나 스키마를 검사하지 않아, 실제 데이터베이스에서 실패할 코드가 그대로 통과합니다. 최소한의 통합 검사가 따로 필요합니다.

06비동기 테스트

케이스 배열을 for...of로 돌며 동기 비교 결과를 출력합니다. 각 케이스가 비동기라면 루프 안에서 await를 걸어야 하는데, 이때 forEach로 바꾸면 콜백 안의 await를 바깥이 기다려 주지 않습니다. for...of는 순서대로 기다릴 수 있어 이 상황에 더 적합합니다.

JavaScript 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예외 테스트

값을 받아 기댓값과 비교하는 흐름만 있고 예외를 잡는 부분은 없습니다. 던져지는지 확인하려면 try/catch로 감싼 뒤 catch에 도달하지 못한 경우를 실패로 처리해야 합니다. async 함수가 던진 오류는 거부된 Promise가 되므로, await 없이 호출하면 처리되지 않은 거부로만 남고 테스트는 통과한 것처럼 보입니다.

JavaScript code

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

던져지는지 확인할 때는 try 블록 끝에 실패를 알리는 줄을 두어, 예외가 나지 않은 경우도 반드시 드러나게 만들어야 합니다.

자주 하는 실수

기다리지 않고 부른 async 함수가 던지면 거부된 프로미스로만 남아 테스트는 통과한 것처럼 보입니다. 반환 프로미스를 반드시 기다려야 합니다.

08속성 스타일 테스트

반복 1000회에 걸린 시간을 재서 출력하는 코드이며, 입력을 만들어 내는 부분은 없습니다. 속성 기반 테스트는 임의 입력을 대량으로 생성해 항상 성립해야 할 성질을 검사하는 방식입니다. 무작위 입력을 쓸 때는 실패를 다시 재현할 수 있도록 사용한 시드를 반드시 남겨야 합니다.

JavaScript code

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

속성 기반 테스트는 개별 입력 대신 언제나 성립해야 할 관계를 검사하므로, 사람이 떠올리지 못한 입력에서 반례를 찾아 줍니다.

자주 하는 실수

무작위 입력을 쓰면서 시드를 남기지 않으면 실패를 다시 만들 수 없습니다. 로그에 시드가 없으면 그 실패는 사실상 조사할 수 없습니다.

09스냅샷 스타일 테스트

조건이 어긋날 때 throw하는 한 줄 단언일 뿐, 저장된 기대 출력과 대조하지는 않습니다. 스냅샷은 결과 전체를 파일로 남긴 뒤 다음 실행에서 통째로 비교하는 방식입니다. 편하지만 사람이 내용을 확인하지 않고 기대 파일을 갱신해 버리면 잘못된 출력이 정답으로 굳어집니다.

JavaScript code

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

스냅샷은 출력 형식이 자주 바뀌지 않는 대상에 잘 맞고, 기대 파일을 코드와 함께 검토 대상에 올려야 변경이 사람 눈에 들어옵니다.

자주 하는 실수

실패할 때마다 기대 파일을 자동 갱신하면 잘못된 출력이 정답으로 굳습니다. 갱신하기 전에 차이를 사람이 확인하는 절차가 있어야 합니다.

10통합 테스트 스케치

입력과 기댓값 쌍을 미리 늘어놓고 하나씩 확인하는 뼈대입니다. 통합 테스트도 같은 표 형태로 시나리오를 나열할 수 있지만, 각 케이스가 실제 자원을 건드리므로 실행 후 상태를 되돌려 놓아야 합니다. 케이스가 앞선 실행이 남긴 데이터에 기대기 시작하면 순서를 바꾸는 순간 깨집니다.

JavaScript 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파서 테스트

이행된 Promise의 결과가 기대한 숫자인지 확인하는 최소한의 비동기 검사입니다. 파서를 검증한다면 입력 문자열과 기대 트리를 쌍으로 두고 같은 자리에서 비교하게 됩니다. 파서는 정상 입력보다 깨진 입력에서 어떤 오류를 어디 위치로 보고하는지가 더 중요하므로 실패 케이스를 함께 넣어야 합니다.

JavaScript code

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

파서는 정상 입력보다 깨진 입력이 중요합니다. 어떤 오류를 어느 위치로 보고하는지까지 검증해야 사용자에게 쓸모 있는 메시지가 유지됩니다.

자주 하는 실수

이미 이행된 프로미스만으로 검사하면 실제 지연이나 거부 경로가 전혀 확인되지 않습니다. 실패하는 케이스도 함께 넣어야 의미가 있습니다.

12컬렉션 테스트

빈 루프를 1000번 돌린 시간을 출력할 뿐 컬렉션을 다루지는 않습니다. 배열이나 Map을 검사할 때는 길이만이 아니라 순서와 중복까지 봐야 합니다. 특히 ===로는 배열 내용을 비교할 수 없어 요소별로 돌거나 깊은 비교 함수를 따로 써야 합니다.

JavaScript code

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

배열 비교에는 node:assert의 deepStrictEqual이 순서와 타입까지 함께 확인해 줍니다. 순서가 무의미하다면 정렬한 뒤 비교해야 합니다.

자주 하는 실수

[1, 2] === [1, 2]는 참조 비교라 항상 거짓입니다. 이 조건으로 만든 테스트는 어떤 구현을 넣어도 실패해 원인을 엉뚱한 곳에서 찾게 됩니다.

13문자열 테스트

숫자 비교가 어긋날 때만 예외를 던지는 형태이며 문자열은 등장하지 않습니다. 문자열을 검사할 때는 눈에 보이지 않는 공백이나 줄바꿈이 실패 원인인 경우가 많습니다. 유니코드 결합 문자 때문에 겉보기에 같은 글자도 ===에서는 다를 수 있어 normalize()가 필요할 때가 있습니다.

JavaScript code

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

실패한 문자열 비교는 눈으로 구분되지 않는 경우가 많아, 값을 JSON.stringify로 감싸 찍으면 공백과 줄바꿈이 그대로 드러납니다.

자주 하는 실수

결합 문자로 표현된 한글이나 악센트는 겉보기가 같아도 엄격 비교에서 다릅니다. 비교 전에 normalize()로 표현 형태를 맞춰야 합니다.

14매개변수화 테스트

케이스 객체에서 input과 expected를 꺼내 같은 식에 대입하며 반복합니다. 검사 로직은 한 번만 쓰고 데이터만 늘리는 구조라 새 조건을 추가하는 비용이 거의 없습니다. 대신 어긋난 케이스가 어떤 항목이었는지 함께 출력하지 않으면 실패 로그만 보고는 원인을 찾을 수 없습니다.

JavaScript 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 한 번과 값 비교만 있고 시간을 재는 부분은 들어 있지 않습니다. 벤치마크를 붙이려면 같은 작업을 여러 번 반복해 평균을 내고 예열 구간은 따로 버려야 합니다. JIT 컴파일러가 결과를 쓰지 않는 계산을 통째로 없앨 수 있으므로, 측정 대상의 반환값은 어딘가에 남겨 두어야 합니다.

JavaScript code

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

벤치마크는 예열 구간을 버리고 여러 번 반복한 뒤 분포를 봐야 합니다. 평균 하나만 보면 이따금 튀는 값에 가려 판단이 흔들립니다.

자주 하는 실수

결과를 어디에도 쓰지 않는 계산은 JIT가 통째로 없앨 수 있어 측정 시간이 0에 가깝게 나옵니다. 측정 대상의 반환값은 반드시 어딘가에 남겨야 합니다.

정리하며

  • 시간과 저장소는 인자로 주입해 목 객체로 갈아 끼울 수 있게 만듭니다
  • 케이스가 셋을 넘어가면 테이블 기반으로 바꿔 중복 코드를 없앱니다
  • await를 빠뜨린 비동기 테스트는 단언이 실패해도 통과로 보고됩니다
  • 가짜 타이머와 목 객체는 테스트가 끝날 때 반드시 원상복구합니다

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