跳到主要内容

测试

测试金字塔:单元测试覆盖核心逻辑,集成测试验证下游协作,E2E 测试保障关键链路。

测试分层​

层级范围工具覆盖率目标
单元测试service / provider / 工具函数Vitest核心 service ≥ 80%
集成测试网关 → service → 下游 mockVitest + Fastify inject关键 API 全通过
E2E 测试完整容器环境(真实 Redis/Kafka)Vitest + Fastify inject核心用户旅程全通过

单元测试​

pnpm add -D vitest vite-tsconfig-paths @vitest/coverage-v8 @nestjs/testing
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
plugins: [tsconfigPaths()],
test: {
globals: true,
include: ['**/*.spec.ts'],
},
});

globals: true 时在 tsconfig.json 补充全局类型声明:

{ "compilerOptions": { "types": ["vitest/globals"] } }
// package.json scripts
{
"test": "vitest run",
"test:watch": "vitest",
"test:cov": "vitest run --coverage",
"test:e2e": "vitest run --config ./vitest.config.e2e.ts"
}
// modules/notification/notification.service.spec.ts
describe('NotificationService', () => {
let service: NotificationService;
let springClient: SpringApiClient;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
NotificationService,
{
provide: SpringApiClient,
useValue: { getTenantStats: vi.fn() },
},
],
}).compile();

service = module.get(NotificationService);
springClient = module.get(SpringApiClient);
});

it('聚合租户通知数据', async () => {
vi.mocked(springClient.getTenantStats).mockResolvedValue({ unread: 5 });
const result = await service.aggregate('tenant-1');
expect(result.unread).toBe(5);
});
});

Mock 原则:

  • 下游 HTTP 调用(SpringApiClient)必须 mock,不发起真实网络请求
  • Redis 操作使用 ioredis-mock 或内存实现,不连接真实 Redis
  • Kafka 事件(kafkajs)mock 处理器注册或 Producer,不连接真实 broker

E2E 测试​

E2E 测试在独立容器中运行,连接真实 Redis 与 Kafka(Docker Compose 编排)。独立的 vitest.config.e2e.ts 与单元测试隔离(不同 include 模式,可独立编排执行时机):

// vitest.config.e2e.ts
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
plugins: [tsconfigPaths()],
test: {
globals: true,
include: ['test/**/*.e2e-spec.ts'],
},
});
// test/app.e2e-spec.ts
describe('AppController (e2e)', () => {
let app: NestFastifyApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication(new FastifyAdapter());
await app.init();
await app.getHttpAdapter().getInstance().ready();
});

afterAll(async () => {
await app.close();
});

it('/api/health (GET)', () => {
return app.inject({ method: 'GET', url: '/api/health' })
.then((res) => {
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).status).toBe('ok');
});
});

it('GraphQL 聚合查询', () => {
const query = `{ dashboard { tenantId stats { unread } } }`;
return app.inject({
method: 'POST',
url: '/graphql',
headers: { authorization: 'Bearer test-token' },
payload: { query },
}).then((res) => {
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.payload).data.dashboard).toBeDefined();
});
});

测试数据管理​

  • 测试数据在 beforeAll 中通过工厂函数构造,测试间隔离
  • 敏感字段(密码、令牌)使用固定测试值,不使用真实数据
  • 测试完成后清理 Redis key 与 Kafka topic(或按测试用例命名隔离)