2편. Vitest 테스트 구조 설계와 Assertion, Matcher 이해하기
📚 목차
1. 테스트 선언 API : 테스트 구조 설계 이해하기
2. Assertion 진입점 API - expect() 이해하기
3. 주요 Matcher API 이해하기 - 값, 구조, 행위 검증
📂 [GitHub 예시 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /vitest
1. 테스트 선언 API : 테스트 구조 설계 이해하기
🔷 테스트 계층 구조 : describe와 it(test)
테스트는 크게 그룹(Context)과 사례(Case)로 나뉩니다.
1) describe
관련된 테스트 케이스들을 하나의 논리적 그룹으로 묶습니다.
테스트 대상의 범위(Context)를 명확하게 표현합니다.
▸ 특정 클래스
▸ 특정 함수 또는 메서드
▸ 특정 시나리오 (예: 로그인 실패 케이스, 권한 오류 처리 등)
describe('UserService', () => {
// UserService 관련 테스트들
});
2) it (또는 test): 실제 검증이 수행되는 최소 단위
하나의 기대 동작(Assertion)을 검증하는 테스트 케이스입니다.
실제 실행과 검증 로직이 들어가는 부분입니다.
it('should return 200 status', () => {
// assertion logic
});
🔸 it vs test : 기능적으로 완전히 동일하며, 가독성과 표현 스타일의 차이입니다.
🔷 중첩(Nesting) 전략
테스트를 단순히 나열하기보다는, 상태(Context) 중심으로 중첩 구조를 설계하는 것이 유지보수성과 가독성 측면에서 매우 유리합니다.
describe(테스트 대상)
└─ describe(기능 / 메서드)
└─ describe(특정 상황 / 조건)
└─ it(기대 결과)
> Top-level: 서비스 또는 모듈
> Mid-level: 특정 메서드
> Low-level: 입력 조건, 상태, 예외 케이스
> it: 기대 결과
예시)
// ProductService 전체에 대한 테스트 그룹
describe('ProductService', () => {
// ProductService 안의 calculateDiscount() 메서드를 검증하는 그룹
describe('calculateDiscount()', () => {
// [상황 A] 사용자가 VIP 회원일 때의 조건을 정의
describe('when the user is a VIP', () => {
// VIP 회원이면 20% 할인이 적용되어야 함을 검증
it('20% 할인율을 적용해야 한다', () => {
// Arrange: 테스트에 필요한 데이터 준비
// Act: calculateDiscount() 실행
// Assert: 결과가 20% 할인인지 검증
});
});
// [상황 B] 사용자가 일반 회원일 때의 조건을 정의
describe('when the user is a regular member', () => {
// 일반 회원이면 10% 할인이 적용되어야 함을 검증
it('10% 할인율을 적용해야 한다', () => {
// Arrange: 테스트에 필요한 데이터 준비
// Act: calculateDiscount() 실행
// Assert: 결과가 10% 할인인지 검증
});
});
});
});
🔷 병렬 실행(Parallel Execution)의 기본 원리
테스트 실행 속도는 개발 생산성과 직결됩니다.
Vitest와 Jest는 기본적으로 병렬 실행 구조를 채택하고 있습니다.
1) 서로 다른 테스트 파일은 별도의 워커(프로세스/스레드)에서 병렬로 실행됩니다.
병렬 실행 환경에서는 테스트 간 상태 공유가 절대 금물입니다.
auth.test.ts ─▶ Worker A
user.test.ts ─▶ Worker B
order.test.ts ─▶ Worker C
2) 파일 내부는 기본적으로 순차 실행
하나의 파일 안에서 정의된 describe와 it는 선언된 순서대로 실행됩니다.
2. Assertion 진입점 API - expect() 이해하기
🔷 expect()는 무엇인가
expect()는 테스트에서 검증할 대상(Actual / Received)을 “Assertion 체인”에 올려두는 함수입니다.
▸ 검증하고 싶은 값(함수의 반환값, API 응답, 객체 상태 등)을 expect()에 넣습니다.
▸ 그 다음에 .toBe(), .toEqual(), .toThrow() 같은 matcher를 붙여서 “어떻게 검증할지”를 선언합니다.
expect(actual).toBe(expected);
1. actual (실제값): 검증하고 싶은 대상입니다. 함수 호출 결과, 변수 값 등이 들어갑니다.
2. expect(): 이 실제값을 테스트 환경이 이해할 수 있는 객체로 감쌉니다(Wrapping). 이제 이 객체는 다양한 'Matcher'를 사용할 수 있는 상태가 됩니다.
3. Matcher (.toBe 등): 실제값과 기대값을 비교하는 비교 규칙입니다.
4. expected (기대값): 우리가 "이럴 것이다"라고 예상하는 정답지입니다.
🔷 Assertion 실패 메시지 해석
테스트가 실패하면 러너는 왜 실패했는지 최소 단서를 제공합니다.
대표적으로 아래 3가지를 보여줍니다.
1. 어떤 assertion이 실패했는지
2. 어떤 matcher를 썼는지
3. Expected vs Received 차이
예시 코드
test('더하기 테스트', () => {
const result = 1 + 1;
expect(result).toBe(3); // 일부러 틀린 기댓값을 넣었습니다.
});
출력결과

(1) expect(received).toBe(expected)
어떤 형태의 assertion이 실패했는지 알려줍니다.
즉, “received와 expected를 toBe로 비교했다”는 뜻입니다.
(2) // Object.is equality
toBe가 어떤 비교 규칙을 쓰는지 힌트입니다.
여기서는 Object.is 기반 동일성 비교였음을 명확히 알려줍니다.
(3) Expected vs Received
Expected: 테스트 작성자가 기대한 값 (정답으로 가정한 값)
Received: 실제 코드 실행 결과 (현재 코드가 내놓은 값)
(4) 코드 위치 표시
❯ tests/ch01/math.test.ts:12:20
10| test('더하기 테스트', () => {
11| const result = 1 + 1;
12| expect(result).toBe(3); // 일부러 틀린 기댓값을 넣었습니다.
| ^
13| });
3. 주요 Matcher API 이해하기 - 값, 구조, 행위 검증
API 참고 문서: https://vitest.dev/api/
1. 값의 일치 여부 확인 (Equality)
가장 기본이 되는 Matcher들입니다. "단순히 값이 같은가?" 혹은 "메모리 주소까지 같은가?"에 따라 선택이 달라집니다
| Matcher | 설명 | 예시 |
| toBe() | 원시값(Primitive) 비교 및 참조 동일성 비교 (Object.is 기반) | 숫자, 문자열, boolean 비교, 동일 객체(메모리 주소) 비교 |
| toEqual() | 객체·배열의 값(구조) 비교. 모든 필드를 재귀적으로 검사 | API 응답 객체 검증, DTO 구조 검증 |
| toStrictEqual() | toEqual()보다 엄격함. 타입, undefined 필드, 클래스 구조까지 검사 | 클래스 인스턴스 비교, 정밀한 데이터 정합성 검증 |
it('Equality Matchers 실습', () => {
const user = { name: 'Gemini' };
const sameUser = user;
const anotherUser = { name: 'Gemini' };
// 1. toBe: 참조가 같아야 성공
expect(user).toBe(sameUser);
// expect(user).toBe(anotherUser); // ❌ 실패 (내용은 같으나 메모리 주소가 다름)
// 2. toEqual: 내용(구조)만 같으면 성공
expect(user).toEqual(anotherUser); // ✅ 성공
// 3. toStrictEqual: 타입과 undefined 여부까지 확인
class User { name = 'Gemini'; }
expect(new User()).toEqual({ name: 'Gemini' }); // ✅ 성공
// expect(new User()).toStrictEqual({ name: 'Gemini' }); // ❌ 실패 (class와 일반 object의 차이)
});
2. 참/거짓 및 상태 검증 (Truthiness)
| Matcher | 설명 |
| toBeNull() | 오직 null인 경우에만 통과합니다. |
| toBeUndefined() | 오직 undefined인 경우에만 통과합니다. |
| toBeDefined() | toBeUndefined()의 반대 개념으로, 값이 정의되어 있으면 통과합니다. |
| toBeTruthy() | if 문에서 true로 평가되는 모든 값이 통과합니다.예: 1, "hello", [], {} |
| toBeFalsy() | if 문에서 false로 평가되는 모든 값이 통과합니다.예: 0, "", null, undefined, NaN |
describe('1. Truthiness 실습', () => {
test('null 값의 상태 검증', () => {
const value = null;
expect(value).toBeNull(); // ✅ null인지 확인
expect(value).toBeDefined(); // ✅ 정의는 되어 있음 (undefined가 아님)
expect(value).not.toBeUndefined(); // ✅ undefined가 아님
expect(value).not.toBeTruthy(); // ❌ null은 참이 아님
expect(value).toBeFalsy(); // ✅ null은 거짓 같은 값(Falsy)임
});
test('undefined 값의 상태 검증', () => {
const value = undefined;
expect(value).toBeUndefined(); // ✅ undefined인지 확인
expect(value).not.toBeNull(); // ❌ null은 아님
expect(value).not.toBeTruthy(); // ❌ 참이 아님
expect(value).toBeFalsy(); // ✅ undefined는 거짓 같은 값(Falsy)임
});
test('일반 값들의 Truthy/Falsy 판별', () => {
// Falsy 예시
expect("").toBeFalsy(); // 빈 문자열은 Falsy
expect(0).toBeFalsy(); // 숫자 0은 Falsy
// Truthy 예시
expect("Hello").toBeTruthy(); // 문자열이 있으면 Truthy
expect(1).toBeTruthy(); // 0이 아닌 숫자는 Truthy
expect([]).toBeTruthy(); // 빈 배열은 Truthy (JS 특징)
expect({}).toBeTruthy(); // 빈 객체는 Truthy
});
});
3. 숫자 검증 (Numbers)
| matcher | 기준 | 설명 |
| toBeGreaterThan(n) | > | 값이 n보다 큰지 검증 |
| toBeGreaterThanOrEqual(n) | ≥ | 값이 n보다 크거나 같은지 검증 |
| toBeLessThan(n) | < | 값이 n보다 작은지 검증 |
| toBeLessThanOrEqual(n) | ≤ | 값이 n보다 작거나 같은지 검증 |
| toBeCloseTo(expected, precision?) | 반올림 비교 | 소수점 오차를 허용하여 비교 |
describe('2. Numbers 실습', () => {
it('숫자 크기 비교 검증', () => {
const weight = 75 + 5; // 80
expect(weight).toBeGreaterThan(70); // 80 > 70
expect(weight).toBeGreaterThanOrEqual(80); // 80 >= 80
expect(weight).toBeLessThan(100); // 80 < 100
expect(weight).toBeLessThanOrEqual(80); // 80 <= 80
// 정확히 일치하는지 확인 (두 개 모두 사용 가능)
expect(weight).toBe(80);
expect(weight).toEqual(80);
});
it('부동 소수점 오차 검증 (가장 중요)', () => {
const result = 0.1 + 0.2; // 실제 값: 0.30000000000000004
// expect(result).toBe(0.3);
// ❌ 위 코드는 실패합니다. JS의 부동 소수점 오차 때문입니다.
// 해결책: toBeCloseTo를 사용합니다.
// 두 번째 인자 '5'는 소수점 5째 자리까지 반올림해서 확인하겠다는 뜻입니다.
expect(result).toBeCloseTo(0.3, 5);
});
it('금융/수량 관련 경계값 테스트 예시', () => {
const balance = 1500.55;
// 최소 잔액 기준 확인
expect(balance).toBeGreaterThan(1000);
// 정확한 소수점 금액 확인
expect(balance).toBeCloseTo(1500.55, 2);
});
});
4. 문자열과 배열 검증 (Strings & Arrays)
| Matcher | 용도 | 설명 |
| toMatch(regex) | 문자열 패턴 검사 | 정규표현식을 사용하여 문자열이 특정 형식(이메일, 전화번호 등)을 따르는지 확인합니다. |
| toContain(item) | 배열·문자열 포함 여부 검사 | 배열에 특정 요소가 존재하는지, 또는 문자열에 특정 부분 문자열이 포함되어 있는지 확인합니다. |
| toContainEqual(obj) | 배열 내 객체 | 배열 안에 특정 '값을 가진 객체'가 있는지 확인 (참조 무관) |
| toHaveLength(len) | 길이(Size) 검사 | 배열의 요소 개수나 문자열의 길이를 직접 검증합니다. |
import { it, expect, describe } from 'vitest';
describe('데이터 컬렉션 및 문자열 검증', () => {
it('배열(Array) 검증: 목록에 아이템이 포함되어 있는가?', () => {
const shoppingList = ['Apple', 'Banana', 'Orange'];
// 1. 특정 아이템이 포함되어 있는지 확인
expect(shoppingList).toContain('Banana');
// 2. 배열의 전체 크기가 예상과 일치하는지 확인
expect(shoppingList).toHaveLength(3);
// 3. (심화) 객체 배열의 경우
const users = [{ id: 1, name: 'Kim' }, { id: 2, name: 'Lee' }];
expect(users).toContainEqual({ id: 1, name: 'Kim' });
});
it('문자열(String) 검증: 형식이 올바른가?', () => {
const welcomeMessage = '안녕하세요, Vitest의 세계에 오신 것을 환영합니다!';
const email = 'test-user@google.com';
// 1. 특정 문구가 포함되어 있는지 확인
expect(welcomeMessage).toContain('Vitest');
// 2. 정규표현식을 이용한 패턴 매칭 (이메일 형식이 맞는지)
expect(email).toMatch(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
// 3. 특정 단어로 끝나는지 확인
expect(email).toMatch(/com$/);
});
});
5. 객체의 구조 매칭 (Partial Matching)
| matcher | 설명 |
| toMatchObject(obj) | 원본 객체에 기대 객체의 키/값 쌍이 모두 포함되어 있으면 통과합니다. 원본 객체에 다른 속성이 더 많아도 무관합니다. |
| toHaveProperty(path, val?) | 객체 내부에 특정 키가 존재하는지 확인합니다. 점 표기법(a.b.c)으로 중첩된 속성도 탐색할 수 있습니다. val을 지정하면 해당 값까지 함께 검증합니다. |
import { it, expect, describe } from 'vitest';
describe('객체 부분 매칭 및 속성 검증', () => {
const userResponse = {
id: 1,
name: 'Gemini',
email: 'ai@google.com',
settings: {
theme: 'dark',
notifications: true,
},
lastLogin: new Date().toISOString() // 매번 변하는 값
};
it('toMatchObject: 중요한 필드만 골라서 검증', () => {
// lastLogin이나 id가 무엇이든, 이름과 이메일만 맞으면 통과!
expect(userResponse).toMatchObject({
name: 'Gemini',
email: 'ai@google.com'
});
// 중첩된 객체의 일부도 검증 가능
expect(userResponse).toMatchObject({
settings: { theme: 'dark' }
});
});
it('toHaveProperty: 특정 속성의 존재와 값을 확인', () => {
// 1. 단순히 속성이 존재하는지 확인
expect(userResponse).toHaveProperty('id');
// 2. 중첩된 경로의 값을 확인 (Dot notation 사용)
expect(userResponse).toHaveProperty('settings.theme', 'dark');
// 3. 값의 타입만 확인하고 싶을 때 (비대칭 매칭 활용)
expect(userResponse).toHaveProperty('lastLogin', expect.any(String));
});
});
6. 비대칭 매칭 (Asymmetric Matchers)
비대칭 매칭이란, 전체 객체를 비교할 때 특정 필드에 대해서는 "정확한 값" 대신 "특정 조건"만 만족하면 통과시키는 기법입니다.
| matcher | 의미 | 사례 |
| expect.any(Type) | 특정 타입 또는 생성자의 인스턴스인지 검증 | Number, String, Date, Function 등 타입 검사 |
| expect.anything() | null, undefined가 아닌 값인지 검증 | 값이 존재하기만 하면 되는 경우 (존재 여부 확인) |
| expect.stringContaining(str) | 문자열에 특정 문구가 포함되어 있는지 검증 | 로그 메시지, 에러 문구 일부 확인 |
| expect.stringMatching(regex) | 문자열이 정규식 패턴과 일치하는지 검증 | 토큰, UUID, 해시 등 복잡한 문자열 패턴 검사 |
| expect.arrayContaining(items) | 배열이 특정 요소들을 포함하는지 검증 | 순서와 무관하게 핵심 아이템 존재 여부 확인 |
| expect.objectContaining(obj) | 객체가 특정 프로퍼티 집합을 포함하는지 검증 | 대형 API 응답에서 필요한 필드만 부분 검증 |
import { it, expect, describe } from 'vitest';
describe('비대칭 매칭 (Asymmetric Matchers) 실습', () => {
it('API 응답 결과 검증 (랜덤 데이터 처리)', () => {
// 실제 서버에서 올 법한 무작위 데이터
const apiResponse = {
id: 42,
uuid: '550e8400-e29b-41d4-a716-446655440000',
username: 'vitest_tester',
roles: ['admin', 'editor', 'viewer'],
metadata: {
lastLogin: new Date(),
ip: '127.0.0.1'
}
};
expect(apiResponse).toEqual({
// id는 어떤 숫자든 OK
id: expect.any(Number),
// uuid는 정규식 패턴만 맞으면 OK
uuid: expect.stringMatching(/^[a-f0-9-]{36}$/),
// username은 정확히 일치해야 함 (일반 값)
username: 'vitest_tester',
// roles 배열 안에 'admin'과 'editor'가 포함되어 있다면 OK (순서 무관)
roles: expect.arrayContaining(['admin', 'editor']),
// metadata 객체 내부에 lastLogin이 Date 객체이기만 하면 OK
metadata: expect.objectContaining({
lastLogin: expect.any(Date),
ip: expect.anything() // null/undefined만 아니면 됨
})
});
});
it('Partial Match (부분 일치) 활용', () => {
const user = { id: 1, name: 'John', email: 'john@example.com' };
// 전체 객체가 아닌, 내가 관심 있는 필드만 포함되어 있는지 검사
expect(user).toEqual(expect.objectContaining({
email: 'john@example.com'
}));
});
});
7. 예외 및 비동기 처리 (Errors & Promises)
| 상황 | 패턴 | matcher |
| 동기 함수가 에러를 발생시키는 경우 | expect(() => fn()) | .toThrow() |
| 비동기 함수가 정상적으로 성공하는 경우 | await expect(fn()) | .resolves.toBe(...) |
| 비동기 함수가 에러를 발생시키는 경우 | await expect(fn()) | .rejects.toThrow() |
📌 핵심 주의사항: 함수로 감싸기
expect(failGracefully())처럼 직접 실행하면, Vitest가 검사하기도 전에 코드가 멈춰버립니다. 반드시 () => ...와 같이 익명 함수로 한 번 감싸서 전달해야 Vitest가 해당 함수를 실행하며 에러를 가로챌 수 있습니다.
예시: 에러 발생 검증 (toThrow)
const withdrawMoney = (amount: number) => {
if (amount < 0) throw new Error('음수 금액은 출금할 수 없습니다.');
};
it('출금 에러 검증', () => {
// 1. 에러가 발생하는지만 확인
expect(() => withdrawMoney(-100)).toThrow();
// 2. 특정 에러 메시지가 포함되어 있는지 확인 (가장 많이 쓰임)
expect(() => withdrawMoney(-100)).toThrow('음수 금액');
// 3. 특정 에xtension(클래스) 타입의 에러인지 확인
expect(() => withdrawMoney(-100)).toThrow(Error);
});
예시: 비동기 검증(resolves, rejects)
const fetchUser = async (id: number) => {
if (id <= 0) throw new Error('Invalid ID');
return { id, name: 'User' + id };
};
it('비동기 성공 및 실패 검증', async () => {
// 성공(Resolve) 검증
await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'User1' });
// 실패(Reject) 검증
await expect(fetchUser(0)).rejects.toThrow('Invalid ID');
});
예시: 비동기 검증(await)
it('await를 직접 활용한 검증', async () => {
const result = await fetchUser(10);
expect(result.id).toBe(10);
expect(result.name).toContain('User');
});
예시: 비동기 에러의 트릭 (try-catch 패턴)
it('try-catch를 이용한 정밀한 에러 검증', async () => {
try {
await fetchUser(-1);
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(error.message).toBe('Invalid ID');
}
});
8. 호출 여부 검증 (Spies & Mocks)
상태(결과값)가 아닌 행위(함수가 어떻게 호출되었나)를 테스트합니다.
외부 API 호출, 로그 기록, 이벤트 핸들러 실행 여부 등을 확인할 때 사용합니다.
| matcher | 설명 |
| toHaveBeenCalled() | 함수가 최소 한 번이라도 실행되었는지 검증합니다. |
| toHaveBeenCalledTimes(n) | 정확히 n번 실행되었는지 확인합니다.반복문 내부 로직 검증에 유용합니다. |
| toHaveBeenCalledWith(...args) | 호출 시 전달된 인자들이 예상값과 일치하는지 검증합니다. |
| toHaveBeenLastCalledWith(...args) | 여러 번 호출되었을 때, 가장 마지막 호출에 전달된 인자를 확인합니다. |
import { it, expect, vi, describe } from 'vitest';
describe('함수 호출 행위 검증', () => {
it('Spies & Mocks 실전 활용', () => {
// 1. 가짜 함수(Mock) 생성
const sendNotification = vi.fn((message: string, code?: number) => true);
// 2. 함수 실행 (실제로는 특정 비즈니스 로직 내부에서 실행됨)
sendNotification('결제가 완료되었습니다.', 200);
sendNotification('배송이 시작되었습니다.', 300);
// 3. 검증 시작
// 최소 한 번은 실행되었는가?
expect(sendNotification).toHaveBeenCalled();
// 정확히 2번 실행되었는가?
expect(sendNotification).toHaveBeenCalledTimes(2);
// 첫 번째 호출 때 어떤 인자를 받았는가? (정밀 검증)
// 숫자는 정확한 값 대신 '어떤 숫자든 상관없음'으로 유연하게 검증 가능
expect(sendNotification).toHaveBeenCalledWith('결제가 완료되었습니다.', expect.any(Number));
// 가장 마지막 호출의 인자는 무엇인가?
expect(sendNotification).toHaveBeenLastCalledWith('배송이 시작되었습니다.', 300);
});
it('객체 인자 검증 (Partial Matching)', () => {
const updateUser = vi.fn();
updateUser({ id: 1, name: 'Alice', role: 'admin' });
// 객체의 모든 속성을 다 적지 않고, 특정 속성이 포함되어 있는지만 확인
expect(updateUser).toHaveBeenCalledWith(expect.objectContaining({ name: 'Alice' }));
});
});
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'4.Node.js > Vitest&TypeBox' 카테고리의 다른 글
| [Vitest] 6편. MSW(Mock Service Worker) + Vitest 실무 API 테스트 (0) | 2026.02.06 |
|---|---|
| [Vitest] 5편. Vitest로 외부 API 테스트하기: axios, undici (0) | 2026.02.05 |
| [Vitest] 4편. Vitest로 Web API 테스트하기: Fastify inject() 활용법 (0) | 2026.01.30 |
| [Vitest] 3편. Vitest의 Lifecycle · Async · Execution Control API (0) | 2026.01.28 |
| [Vitest] 1편. Vitest 4로 Node.js 테스트 환경 구축하기 (0) | 2026.01.26 |