testing-vitest
Use when writing tests for NestJS + MikroORM with transaction rollback and real DB integration.
$ 설치
git clone https://github.com/bloominggrace/create-fullstack-service /tmp/create-fullstack-service && cp -r /tmp/create-fullstack-service/.cursor/skills/testing-vitest ~/.claude/skills/create-fullstack-service// tip: Run this command in your terminal to install the skill
SKILL.md
name: testing-vitest description: Use when writing tests for NestJS + MikroORM with transaction rollback and real DB integration.
Vitest Testing Guide
Use this skill when writing tests (*.spec.ts, *.e2e-spec.ts) for NestJS + MikroORM projects.
Test Types
- Integration Tests (*.spec.ts): Service layer, uses real DB
- E2E Tests (*.e2e-spec.ts): Full API endpoint flow
⚠️ Do NOT write controller unit tests ⚠️ Use real DB connection instead of mocks
Service Integration Test Structure
import { faker } from '@faker-js/faker';
import { type EntityManager, MikroORM } from '@mikro-orm/core';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { ConflictException, NotFoundException } from '@nestjs/common';
import { Test, type TestingModule } from '@nestjs/testing';
import { plainToInstance } from 'class-transformer';
import { validateOrReject } from 'class-validator';
import { CreateEntityDto } from './dto/create-entity.dto';
import { Entity } from './entities/entity.entity';
import { EntityService } from './entity.service';
async function generateCreateEntityDto(): Promise<CreateEntityDto> {
const dto = plainToInstance(CreateEntityDto, {
field: faker.lorem.word(),
});
await validateOrReject(dto);
return dto;
}
describe('EntityService 테스트', () => {
let testingModule: TestingModule;
let mikroOrm: MikroORM;
let entityService: EntityService;
let entityManager: EntityManager;
beforeAll(async () => {
testingModule = await Test.createTestingModule({
imports: [MikroOrmModule.forRoot(), MikroOrmModule.forFeature([Entity])],
providers: [EntityService],
}).compile();
mikroOrm = testingModule.get(MikroORM);
entityService = testingModule.get(EntityService);
});
beforeEach(async () => {
entityManager = mikroOrm.em.fork();
Object.assign(entityService, { entityManager });
await entityManager.begin();
});
afterEach(async () => {
await entityManager.rollback();
});
afterAll(async () => {
await testingModule.close();
});
describe('생성', () => {
test('신규 엔티티를 생성한다.', async () => {
const dto = await generateCreateEntityDto();
const entity = await entityService.create(dto);
expect(entity.id).toBeDefined();
});
});
});
Transaction Rollback Pattern
Each test must run independently:
beforeEach(async () => {
entityManager = mikroOrm.em.fork();
Object.assign(entityService, { entityManager });
await entityManager.begin(); // Start transaction
});
afterEach(async () => {
await entityManager.rollback(); // Rollback to clean data
});
E2E Test Structure
import { faker } from '@faker-js/faker';
import { type INestApplication, ValidationPipe } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { Logger } from 'nestjs-pino';
import request from 'supertest';
import { type App } from 'supertest/types';
import { AppModule } from '../src/app.module';
describe('E2E 테스트', () => {
let app: INestApplication<App>;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useLogger(app.get(Logger));
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('EntityController 테스트', () => {
describe('(GET) /entities/:id', () => {
test('엔티티를 조회한다.', async () => {
const entityId = '019afbdc-b30f-771f-a859-7c17a7dea978';
const response = await request(app.getHttpServer())
.get(`/entities/${entityId}`)
.expect(200);
expect(response.body.id).toEqual(entityId);
});
test('존재하지 않는 엔티티를 조회한다.', async () => {
await request(app.getHttpServer())
.get(`/entities/${faker.string.uuid()}`)
.expect(404);
});
});
});
});
Test Naming Conventions
describe Blocks
- Top level:
{Service/Controller}Service 테스트 - Feature level: Korean description of behavior (e.g.,
사용자 생성,OTP 발송) - HTTP method:
(METHOD) /path/to/endpoint
test Blocks
- Write in Korean with specific scenario description
- End with
~한다.format
describe('사용자 생성', () => {
test('신규 사용자를 생성한다.', async () => { });
test('중복 사용자를 생성한다.', async () => { });
});
DTO Generation Helper Functions
Generate test DTOs using helper functions:
async function generateCreateUserDto(): Promise<CreateUserDto> {
const dto = plainToInstance(CreateUserDto, {
phoneNumber: faker.helpers.arrayElement([
`+82101${faker.string.numeric(7)}`,
`+81901${faker.string.numeric(7)}`,
]),
});
await validateOrReject(dto);
return dto;
}
Test Fixtures
Separate reusable test data into fixture files:
// test/phone-number.fixture.ts
import { faker } from '@faker-js/faker';
export const PHONE_NUMBER_TEST_CASES = [
{ country: '한국', phoneNumber: () => `+82101${faker.string.numeric(7)}` },
{ country: '일본', phoneNumber: () => `+81901${faker.string.numeric(7)}` },
{ country: '중국', phoneNumber: () => `+86131${faker.string.numeric(8)}` },
];
export function generatePhoneNumber(): string {
return faker.helpers.arrayElement(PHONE_NUMBER_TEST_CASES).phoneNumber();
}
test.each Pattern
Repeat tests for multiple cases:
test.each(PHONE_NUMBER_TEST_CASES)(
'$country 전화번호로 OTP를 발송한다.',
async ({ phoneNumber }) => {
const result = await service.sendOtp({ phoneNumber: phoneNumber() });
expect(result.message).toBe('OTP 발송 성공');
}
);
File Structure
src/
├── {module}/
│ └── {module}.service.spec.ts # Service test
test/
└── app.e2e-spec.ts # E2E tests
Vitest Configuration (vitest.config.ts)
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
globalSetup: 'vitest.globalSetup.ts',
include: ['src/**/*.spec.ts', 'test/**/*.e2e-spec.ts'],
},
plugins: [swc.vite()],
});
Global Setup (vitest.globalSetup.ts)
import { MikroORM } from '@mikro-orm/core';
import { TestSeeder } from './mikro-orm.seeders';
export async function setup(): Promise<void> {
const mikroOrm = await MikroORM.init();
await mikroOrm.schema.ensureDatabase();
await mikroOrm.schema.updateSchema();
await mikroOrm.schema.clearDatabase();
await mikroOrm.seeder.seed(TestSeeder);
await mikroOrm.close();
}
Repository

bloominggrace
Author
bloominggrace/create-fullstack-service/.cursor/skills/testing-vitest
1
Stars
1
Forks
Updated2d ago
Added1w ago