3편. Vitest의 Lifecycle · Async · Execution Control API
📚 목차
1. Lifecycle Hook API
2. 비동기 테스트 패턴(async/await)
3. 테스트 실행 제어 API
4. 테스트 컨텍스트(Test Context) API

📂 [GitHub 예시 코드 보러가기] (https://github.com/cericube/nodejs-tutorials) /vitest
1. Lifecycle Hook API
Lifecycle Hook은 테스트 실행 전후에 특정 작업을 수행하기 위한 API입니다.
서버 기동, DB 초기화, Mock 초기화 등 실무에서 매우 자주 사용됩니다.
1. 라이프사이클 실행 순서 시각화
🔸 beforeAll / afterAll
- 테스트 파일 단위로 단 한 번 실행됩니다.
- 공통 리소스 초기화 및 해제에 적합합니다.
🔸 beforeEach / afterEach
- 각 테스트 케이스마다 반복 실행됩니다.
- 테스트 간 상태 격리를 보장하는 역할을 수행합니다.
1. beforeAll (파일 시작 시 1회)
2. beforeEach (각 테스트 시작 전)
3. it/test (테스트 케이스 A 실행)
4. afterEach (각 테스트 종료 후)
5. beforeEach (각 테스트 시작 전)
6. it/test (테스트 케이스 B 실행)
7. afterEach (각 테스트 종료 후)
8. afterAll (파일 종료 시 1회)
일반적으로 “환경 준비는 beforeAll, 상태 초기화는 beforeEach” 로 분리하는 것이 안정적인 패턴입니다.
2. 예시
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
const mockDb = {
users: [] as string[],
connect: async () => console.log('DB 연결 성공'),
disconnect: async () => console.log('DB 연결 종료'),
clear: () => {
mockDb.users = [];
},
};
describe('라이프 사이클 테스트', () => {
beforeAll(async () => {
await mockDb.connect();
});
afterAll(async () => {
await mockDb.disconnect();
});
beforeEach(() => {
mockDb.clear();
console.log('테스트 데이터 초기화.');
});
afterEach(() => {
mockDb.clear();
console.log('테스트 데이터 정리.');
});
it('새로운 사용자를 추가할 수 있다', () => {
mockDb.users.push('Alice');
expect(mockDb.users.length).toBe(1);
expect(mockDb.users).toContain('Alice');
});
it('각 테스트는 독립적인 환경을 가진다(데이터는 비어 있어야 한다.)', () => {
expect(mockDb.users.length).toBe(0);
});
});

3. 중첩된 describe의 실행 순서 이해
describe 블록이 중첩된 경우, 라이프사이클 훅은 바깥 → 안쪽 → 테스트 → 안쪽 → 바깥 순으로 실행됩니다.
describe('outer', () => {
beforeEach(() => console.log('outer beforeEach'));
describe('inner', () => {
beforeEach(() => console.log('inner beforeEach'));
it('test', () => {
console.log('test case body');
});
});
});
실행 순서
outer beforeEach
inner beforeEach
test case body
2. 비동기 테스트 패턴(async/await)
비동기 작업(DB 조회, API 호출 등)은 완료 시점을 예측할 수 없으므로, 테스트 프레임워크가 해당 작업이 끝날 때까지 기다려주도록 만드는 것이 핵심입니다.
1. async / await 패턴 (가장 권장됨)
가장 직관적이며 동기 코드와 유사한 흐름으로 작성할 수 있어 실무에서 표준으로 사용됩니다.
// 테스트 대상: 가상의 DB 조회 함수
async function getUserById(id: number): Promise<User> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: 'Alice', role: 'admin' });
}, 100);
});
}
it('[ASYNC] 존재하는 사용자의 정보를 반환한다', async () => {
const user = await getUserById(1);
expect(user.name).toBe('Alice');
expect(user.role).toBe('admin');
});
2. resolves / rejects
Promise의 상태 변화를 직접 검증할 때 유용하며, 코드가 매우 간결해집니다.
it('[RESOLVES] 성공시 사용자 이름을 포함한다', async () => {
await expect(getUserById(1)).resolves.toMatchObject({ name: 'Alice' });
});
it('[RESOLVES] 잘못된 ID 입력시 에러를 던진다', async () => {
const failJob = () => Promise.reject(new Error('Invalid ID'));
await expect(failJob()).rejects.toThrow('Invalid ID');
});
3. Timeout(타임아웃) 제어 : Default Timeout(= 5초)
네트워크 지연이나 무한 루프 등으로 인해 테스트가 무한정 대기하는 것을 방지합니다.
▸ 개별 테스트 설정: 특정 API 호출이 평소보다 느릴 것으로 예상될 때 사용합니다.
▸ 전역 설정: 프로젝트 전체의 기본 성능 가이드라인을 정할 때 사용합니다.
// 1. 테스트 단위 설정
it('[TIMEOUT] 3초 이내에 외부 API 응답을 받아야 한다.', async () => {
const user = await getUserById(1);
expect(user.name).toBe('Alice');
}, 3000);
// 2. vitest.config.ts (전역 설정)
export default defineConfig({
test: {
// 4. 타임아웃 (유지)
testTimeout: 10_000,
},
});
4. Promise 반환 패턴
async/await가 도입되기 전 주로 사용되던 방식입니다.
테스트 함수에서 Promise를 직접 return 하면 Vitest가 해당 Promise가 처리될 때까지 기다립니다.
it('[PROMISE] Promise를 return하여 비동기를 제어한다', () => {
// return이 없으면 expect가 실행되기 전에 테스트가 종료되어 버림
return getUserById(1).then(user => {
expect(user.id).toBe(1);
});
});
3. 테스트 실행 제어 API
개발 생산성을 높이기 위해서는 전체 테스트를 매번 돌리는 것이 아니라, 필요한 부분만 선택적으로 제어할 줄 알아야 합니다.
1. only와 skip: 특정 테스트 집중 실행 및 제외
복잡한 버그를 수정하거나, 하나의 기능에만 집중하여 디버깅하고 싶을 때는 전체 테스트를 실행하는 것보다 특정 테스트만 선택적으로 실행하는 것이 훨씬 효율적입니다.
Vitest에서는 it.only, describe.only, it.skip, describe.skip API를 통해 이를 손쉽게 제어할 수 있습니다.
describe('장바구니 기능 테스트', () => {
// .only: 이 테스트 파일 내에서 이 테스트만 실행합니다.
it.only('장바구니에 상품을 추가하면 수량이 증가해야 한다', () => {
const cart = new Cart();
cart.add('Apple', 1);
expect(cart.items['Apple']).toBe(1);
});
it('상품 수량은 음수가 될 수 없다', () => {
const cart = new Cart();
// .only가 위에 있으므로, 이 테스트는 무시됩니다.
expect(() => cart.add('Apple', -1)).toThrow();
});
// .skip: 구현이 미완성되었거나 잠시 꺼두어야 할 때 사용합니다.
it.skip('할인 쿠폰 적용 로직 (다음 스프린트 구현 예정)', () => {
// 로직 미구현 상태에서도 전체 테스트 결과에 영향을 주지 않습니다.
});
});
📌 주의:
it.only를 사용한 채로 코드를 커밋하면 CI(지속적 통합) 환경에서 다른 중요한 테스트들이 누락될 수 있습니다.
커밋 전 반드시 제거했는지 확인하세요!
2. describe.concurrent: 테스트 병렬 실행으로 속도 최적화
Vitest는 기본적으로 파일 단위 병렬 실행을 지원하지만, describe.concurrent를 사용하면 하나의 테스트 파일 내부에서도 개별 테스트를 병렬로 실행할 수 있습니다.
병렬 실행 환경에서는 Race Condition, 데이터 충돌, 테스트 비결정성(Flaky Test) 문제가 발생할 수 있으므로, 테스트 간 독립성이 보장되는 경우에만 사용하는 것이 안전합니다.
describe.concurrent('병렬 실행 테스트', () => {
it('테스트 A', async () => {
console.log('[A] start:', Date.now()); // 시작 시각 기록
await delay(1000); // 1초 대기
console.log('[A] end:', Date.now()); // 종료 시각 기록
expect(true).toBe(true);
});
it('테스트 B', async () => {
console.log('[B] start:', Date.now()); // A와 거의 동시에 시작됨
await delay(500); // 0.5초 대기
console.log('[B] end:', Date.now()); // B가 A보다 먼저 끝남
expect(true).toBe(true);
});
});
3. 파일 단위 및 키워드 기반 실행
CLI 환경에서는 특정 파일이나 키워드를 기준으로 테스트를 실행하는 방식이 가장 많이 활용됩니다.
▸ 특정 파일만 실행
npx vitest tests/services/user.service.test.ts
▸ 파일 이름 패턴으로 실행 (예: 'auth'가 들어간 모든 파일):
npx vitest auth
▸ UI 모드 활용
npx vitest --ui
4. Tag 기반 실행 전략: 테스트 성격 분류
프로젝트 규모가 커질수록 테스트를 성격별로 분류하여 실행하는 전략이 중요해집니다.
Vitest는 기본적으로 테스트 이름 패턴 매칭을 통해 태그 기반 실행을 구현할 수 있습니다.
import { it, expect } from 'vitest';
it('[SMOKE] 결제 승인 API 호출', () => {
// 핵심 비즈니스 로직 검증
});
it('[SLOW] 대량의 결제 내역 엑셀 다운로드', () => {
// 시간이 오래 걸리는 작업
});
# 핵심 테스트만 빠르게 실행
npx vitest -t SMOKE
# 느린 테스트 제외 실행 (정규표현식 활용)
px vitest --testNamePattern='^(?!.*SLOW).*'
-t 또는 --testNamePattern 옵션을 활용하면 코드 수정 없이 CLI에서 즉시 실행 대상을 제어할 수 있습니다.

태그예시
| 태그 | 목적 |
| [SMOKE] | 핵심 기능 최소 검증 |
| [SLOW] | 장시간 실행 테스트 |
| [E2E] | 통합 시나리오 테스트 |
| [DB] | 데이터베이스 의존 테스트 |
| [FLAKY] | 불안정 테스트 관리 |
4. 테스트 컨텍스트(Test Context) API
Vitest는 각 테스트 케이스가 실행될 때마다 고유한 Context 객체를 주입합니다.
이를 통해 테스트 내부에서 동적으로 정보를 추출하거나, 테스트 간의 설정을 공유할 수 있습니다.
1. Test Context 기본 구조
테스트 함수의 첫 번째 인자를 구조 분해 할당하여 task, expect 등을 가져올 수 있습니다. 특히 task 객체에는 현재 테스트의 이름, 실행 상태 등 유용한 정보가 담겨 있습니다.
# [실습 예제: 테스트 정보 출력하기]
# task.name: 현재 테스트 케이스의 제목.
# task.type: 테스트 유형 (test, suite 등)
# task.suite: 현재 테스트가 속한 상위 Suite 정보.
# task.id: 테스트의 고유 식별자.
it('Context 기본 구조 테스트', ({ task }) => {
// console.log(`실행 중인 테스트:{task.name}`);
console.log(task);
expect(task.name).toBe('Context 기본 구조 테스트');
expect(task.type).toBe('test'); // 'test' 또는 'suite'
expect(task.id).toBeDefined(); // 테스트 고유 식별자
});
4. 실무 활용 시나리오
▸ 커스텀 Fixture를 통한 속성 부여
test.extend를 사용하면 테스트에 기능명, 중요도 등의 속성을 안정적으로 부여하고 로그 분류나 디버깅에 활용할 수 있습니다.
it.extend 활용:
it 함수를 확장하여 feature 와 severity 라는 고정값(Fixture)을 주입하는 방식은 매우 효율적입니다.
Vitest에서 it()은 내부적으로 test()와 동일한 동작을 수행하며, 단지 별칭(alias)입니다
import { describe, it, expect } from 'vitest';
describe('속성 주입 기본 구조', () => {
// it.extend는 기존 it을 변경하지 않고, 설정이 추가된 '새로운 함수'를 반환합니다.
const billingTest = it.extend({
feature: 'billing',
severity: 'critical',
});
billingTest('결제 실패 시 포인트 롤백 처리 확인', ({ task, feature, severity }) => {
// 결과: [BILLING] Task: 결제 실패 시 포인트 롤백 처리 확인
console.log(`[${feature.toUpperCase()}] Task: ${task.name}`);
console.log(`[Severity] ${severity}`);
expect(severity).toBe('critical');
});
});
# 개별 속성 주입 실습
describe('개별 속성 주입 실습', () => {
// 특정 테스트 하나에만 즉시 확장을 적용합니다.
it.extend({
feature: 'billing',
severity: 'critical',
owner: 'QA-Team',
})('결제 실패 시 롤백 확인', ({ feature, severity, owner }) => {
console.log(`[실습 A] Severity: ${severity}`); // 결과: critical
expect(severity).toBe('critical');
});
/**
* [실습 B] 상속 및 독립성 확인
*/
it.extend({
feature: 'auth',
severity: 'medium', // 명시적으로 medium 설정
owner: 'QA-Team',
})('로그인 세션 만료 테스트', ({ feature, severity, owner }) => {
console.log(`[실습 B] Severity: ${severity}`);
expect(feature).toBe('auth');
expect(severity).toBe('medium');
});
});
▸ 공통 컨텍스트 확장 (hooks 활용)
beforeEach를 사용하면 모든 테스트에서 공통적으로 사용할 리소스(DB 객체, API 클라이언트 등)를 안전하게 주입할 수 있습니다. 이를 통해 코드 중복을 획기적으로 줄입니다.
import { beforeEach, it, expect } from 'vitest';
// 1. 컨텍스트 타입 정의
interface MyTestContext {
db: {
query: (sql: string) => Promise<string>;
status: string;
};
userToken: string;
}
// 2. beforeEach에서 공통 리소스 주입
beforeEach<MyTestContext>((context) => {
context.db = {
query: async (sql) => `Result for ${sql}`,
status: 'Connected',
};
context.userToken = 'mock-auth-token-123';
// 테스트 종료 후 정리(Cleanup) 로직 반환
return () => {
context.db.status = 'Disconnected';
};
});
// 3. 확장된 컨텍스트 사용
it<MyTestContext>('사용자 결제 정보 조회 테스트', async (ctx) => {
const result = await ctx.db.query('SELECT * FROM payments');
console.log(`DB Status: ${ctx.db.status}`);
expect(result).toContain('Result for');
expect(ctx.db.status).toBe('Connected');
});
▸ 커스텀 리포터 연계 및 대시보드 구축
Vitest 설정(vitest.config.ts)에서 커스텀 리포터를 구성하면, 테스트 결과와 메타데이터를 결합하여 QA 자동화 대시보드를 생성할 수 있습니다.
※ 게시된 글 및 이미지 중 일부는 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] 2편. Vitest 테스트 구조 설계와 Assertion, Matcher 이해하기 (0) | 2026.01.27 |
| [Vitest] 1편. Vitest 4로 Node.js 테스트 환경 구축하기 (0) | 2026.01.26 |