关于 nestjs-testing-expert
nestjs-testing-expert 是用于为 NestJS 模块、服务、控制器和 HTTP 端点编写 Vitest 测试的技能。内容涵盖单元、集成和端到端测试,并说明依赖注入、替换提供者及数据库方案。
主要功能
- 01使用 SWC 配置支持 NestJS 元数据的 Vitest
- 02通过明确的协作者测试服务和控制器
- 03替换提供者与守卫,同时保留模块接线
- 04对已启动的 Nest 应用执行 HTTP 流程测试
- 05为单元、集成和端到端测试选择数据库方案
- 06管理清理、状态重置和确定性测试
适用场景
- 01为 NestJS 项目配置 Vitest
- 02模拟外部系统并验证提供者接线
- 03端到端验证需要身份验证的 HTTP 端点
- 04减少不稳定或运行缓慢的测试套件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ shipshitdev/ skills --skill nestjs-testing-expert安装后在 Claude Code 中输入/nestjs-testing-expert确认已加载。
附带文件plugin.json
详细介绍
NestJS 测试专家
为 NestJS 模块、服务、控制器和 HTTP 端点构建可靠的 Vitest 测试套件。
范围
此技能负责 NestJS 机制:测试模块、提供者配置以及请求级端到端流程。对于与框架无关的问题——某个行为应在哪个层级测试、覆盖率数字代表什么、如何设计能经受重构的测试、如何消除偶发失败——请使用 testing-expert。
适用场景
- 为 NestJS 应用编写单元、集成或端到端测试
- 设置测试模块、覆盖提供者或模拟数据库
- 测试控制器、守卫、拦截器或管道
- 在已启动的 Nest 应用上测试 HTTP 端点
Nest 应用中的测试层级
- 单元测试 — 独立测试服务或提供者,并用测试替身替换协作者。除被测提供者外,不涉及模块图。
- 集成测试 — 使用真实提供者编译模块,并模拟外部系统。验证配置和各层之间的契约。
- 端到端测试 — 启动应用并通过 HTTP 驱动。验证完整请求路径:管道、守卫、控制器、服务和持久化。
NestJS 的 Vitest 设置
Nest 的依赖注入会读取装饰器元数据(emitDecoratorMetadata)。Vitest 使用 esbuild 转换代码,而 esbuild 不会生成此元数据,因此构造函数注入会静默解析为 undefined。请使用会生成装饰器元数据的 unplugin-swc 进行编译。
bun add -D vitest unplugin-swc @swc/core @golevelup/ts-vitest supertest @types/supertest
// vitest.config.ts — unit and integration specs
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
root: './',
environment: 'node',
include: ['src/**/*.spec.ts'],
testTimeout: 10_000,
},
plugins: [
swc.vite({
module: { type: 'es6' },
}),
],
});
将端到端测试规范放在单独的配置中,这样 bun run test 就能保持快速,而 bun run test:e2e 会启动应用:
// vitest.config.e2e.ts
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
root: './',
environment: 'node',
include: ['test/**/*.e2e-spec.ts'],
testTimeout: 30_000,
},
plugins: [swc.vite({ module: { type: 'es6' } })],
});
{
"scripts": {
"test": "vitest run",
"test:e2e": "vitest run --config vitest.config.e2e.ts",
"test:cov": "vitest run --coverage"
}
}
旧版运行器配置中的选项与 Vitest 一一对应:路径别名从 moduleNameMapper 转为 resolve.alias(或 vite-tsconfig-paths 插件),testEnvironment 改为 test.environment,transform 由 SWC 插件取代,单个测试套件的超时使用 test.testTimeout 或 vi.setConfig({ testTimeout })。当 globals 关闭时,请从 vitest 显式导入 describe、it、expect、vi 和生命周期钩子。由浏览器驱动的端到端流程应使用 Playwright;此处的 Supertest 用于测试已启动 Nest 应用的 HTTP 接口。
若要自动模拟整个提供者,@golevelup/ts-vitest 中的 createMock 会返回类型与该类匹配的深度模拟对象:
import { createMock, DeepMocked } from '@golevelup/ts-vitest';
服务测试规范
编译一个测试模块,让被测对象使用真实实现,并显式提供每个协作者。注入令牌由相应集成提供——ORM 的令牌辅助函数、类引用或自定义令牌常量。
import { Test, TestingModule } from '@nestjs/testing';
import { afterEach, beforeEach, describe, expect, it, vi, type Mocked } from 'vitest';
describe('UsersService', () => {
let service: UsersService;
let repository: Mocked<UsersRepository>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: UsersRepository, useValue: createUsersRepositoryMock() },
],
}).compile();
service = module.get(UsersService);
repository = module.get(UsersRepository);
});
afterEach(() => {
vi.resetAllMocks();
});
it('returns only the active users of the requested organization', async () => {
const expected = [{ id: '1', organization: 'org1' }];
repository.find.mockResolvedValue(expected);
const result = await service.findAll('org1');
expect(result).toEqual(expected);
expect(repository.find).toHaveBeenCalledWith({
organization: 'org1',
isDeleted: false,
});
});
});
当协作者收到的参数体现了业务规则时,应断言这些参数——上面的 isDeleted: false 筛选条件才是要验证的行为,而不是实现细节。
控制器测试规范
注册控制器,为其服务提供桩,并且只验证控制器自身的职责:提取参数、委托调用和整理响应。业务规则应在服务测试规范中验证。
describe('UsersController', () => {
let controller: UsersController;
let service: Mocked<UsersService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [{ provide: UsersService, useValue: createUsersServiceMock() }],
}).compile();
controller = module.get(UsersController);
service = module.get(UsersService);
});
it('passes the organization from the request through to the service', async () => {
const expected = [{ id: '1', email: '[email protected]' }];
service.findAll.mockResolvedValue(expected);
const result = await controller.findAll('org1');
expect(result).toEqual(expected);
expect(service.findAll).toHaveBeenCalledWith('org1');
});
});
覆盖提供者
编译真实模块,只替换那些不能实际运行的部分。
overrideProvider 和 overrideGuard 能保留其余依赖图的真实性,因此测试仍能验证配置。
const module: TestingModule = await Test.createTestingModule({
imports: [UsersModule],
})
.overrideProvider(MailerService)
.useValue(mailerMock)
.overrideGuard(AuthGuard)
.useValue({ canActivate: () => true })
.compile();
仅在测试其他内容时覆盖守卫。授权本身应通过运行真实守卫的端到端测试进行验证。
集成测试
启动应用,使用临时数据库进行真实持久化,并在测试之间重置状态。
describe('Users (integration)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
await resetDatabase();
});
it('persists a created user and returns it on read', async () => {
const created = await request(app.getHttpServer())
.post('/api/users')
.send({ email: '[email protected]', name: 'Test' })
.expect(201);
const read = await request(app.getHttpServer())
.get(`/api/users/${created.body.id}`)
.expect(200);
expect(read.body.email).toBe('[email protected]');
});
});
在 afterAll 中关闭应用。未关闭的 Nest 应用会使其连接池保持打开状态,导致 Vitest 运行挂起。
端到端测试
以相同方式启动应用,并像真实客户端那样驱动测试——包括身份验证。
describe('Users API (e2e)', () => {
let app: INestApplication;
let authToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
authToken = await signInTestUser(app);
});
afterAll(async () => {
await app.close();
});
it('rejects an unauthenticated list request', () =>
request(app.getHttpServer()).get('/api/users').expect(401));
it('returns the caller organization users when authenticated', () =>
request(app.getHttpServer())
.get('/api/users')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
}));
});
使用共享的 signInTestUser 辅助函数,将令牌流程集中在一处,避免散落在每个测试规范中。
数据库策略
| 方案 | 适用场景 | 成本 |
|---|---|---|
| 仓库测试替身 | 单元测试规范 | 最快;可证明未执行 SQL 或使用架构 |
| 内存或嵌入式引擎 | 集成测试规范 | 快;行为可能与生产环境不一致 |
| 每次运行使用一次性容器 | 集成和端到端测试规范 | 最慢;保真度最高 |
测试之间通过截断数据或回滚事务来重置,而不是重新创建架构——重建架构是测试套件运行时间的主要开销。
提示
- 为每个测试规范单独编译模块;共享模块会导致测试之间泄漏状态。
- 在
afterEach中重置模拟,避免一个测试中的存根影响下一个测试。 - 每个 describe 块启动一次应用程序,并在每个测试中重置数据——每个测试都启动应用程序通常是 Nest 测试套件运行缓慢的原因。
- 保持测试确定性:冻结时钟(使用
vi.useFakeTimers()和vi.setSystemTime),并固定时区,而不是断言实际时区。 - 如果测试规范对共享对象使用
vi.spyOn,请在afterEach中使用vi.restoreAllMocks()恢复间谍。
检查清单
- 结构清晰的准备/执行/断言
- 测试主体是真实的,协作者明确——绝不能反过来
- 覆盖错误和未授权路径,而不只是正常路径
- 在清理阶段关闭应用程序并重置模拟
- 运行速度快