mcpField
N
nestjs-testing-expert

nestjs-testing-expert

github.com/shipshitdev测试与质量38747 次安装2 天前更新
代码仓库

关于 nestjs-testing-expert

nestjs-testing-expert 是用于为 NestJS 模块、服务、控制器和 HTTP 端点编写 Vitest 测试的技能。内容涵盖单元、集成和端到端测试,并说明依赖注入、替换提供者及数据库方案。

主要功能

  1. 01使用 SWC 配置支持 NestJS 元数据的 Vitest
  2. 02通过明确的协作者测试服务和控制器
  3. 03替换提供者与守卫,同时保留模块接线
  4. 04对已启动的 Nest 应用执行 HTTP 流程测试
  5. 05为单元、集成和端到端测试选择数据库方案
  6. 06管理清理、状态重置和确定性测试

适用场景

  1. 01为 NestJS 项目配置 Vitest
  2. 02模拟外部系统并验证提供者接线
  3. 03端到端验证需要身份验证的 HTTP 端点
  4. 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() 恢复间谍。

检查清单

  • 结构清晰的准备/执行/断言
  • 测试主体是真实的,协作者明确——绝不能反过来
  • 覆盖错误和未授权路径,而不只是正常路径
  • 在清理阶段关闭应用程序并重置模拟
  • 运行速度快

评论

支持 Markdown