白留白WHITE SPACE
返回所有文章
NestJS / 后端开发

NestJS 实战 10:单元测试、E2E 测试与交付标准

用单元测试保护关键业务规则。用 E2E 测试覆盖关键使用路径。建立可交付能力:构建、测试、容器化。

本文属于 NestJS BBS 实战教程,共 11 节,建议按顺序学习。

1. 这节课要实现的功能是什么

  • 用单元测试保护关键业务规则。
  • 用 E2E 测试覆盖关键使用路径。
  • 建立可交付能力:构建、测试、容器化。

2. 这节课涉及的 NestJS 知识点有哪些(先讲概念)

2.1 测试分层怎么理解

  • 单元测试:测“某个类的业务逻辑”;
  • E2E 测试:测“模块组合后是否按预期工作”。

2.2 为什么要在 Nest 里 Mock Provider

Nest 的依赖注入天生适合测试替换。你可以在测试模块里用 useValue 注入 mock 仓储,避免依赖真实外部环境。

2.3 @nestjs/testing 的核心价值

它能以接近生产的方式组装模块,让你的测试更贴近真实运行行为。

2.4 交付不仅是“代码写完”

真实项目里,至少要有:

  • npm run test
  • npm run test:e2e
  • npm run build
  • Docker 运行一致性

3. 实际开发中这些 NestJS 知识点被如何使用(详细代码)

3.1 src/users/users.service.mock.spec.ts - Mock Provider 单元测试

import { ConflictException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { USERS_REPOSITORY } from './users.repository';
import { UsersService } from './users.service';

describe('UsersService (mocked repository)', () => {
  it('throws conflict when email already exists', async () => {
    const repo = {
      findByEmail: jest.fn().mockResolvedValue({
        id: 1,
        username: 'alice',
        email: 'alice@example.com',
        passwordHash: 'hash',
        role: 'user',
        createdAt: new Date(),
      }),
      findAll: jest.fn().mockResolvedValue([]),
      create: jest.fn(),
      findById: jest.fn(),
      updateRole: jest.fn(),
    };

    const moduleRef = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: USERS_REPOSITORY, useValue: repo },
      ],
    }).compile();

    const service = moduleRef.get(UsersService);

    await expect(
      service.create({ username: 'alice', email: 'alice@example.com', password: 'password123' }),
    ).rejects.toThrow(ConflictException);
  });
});

3.2 test/auth.e2e-spec.ts - 认证链路测试

import { INestApplication, UnauthorizedException, ValidationPipe } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { AppModule } from '../src/app.module';
import { AuthService } from '../src/auth/auth.service';

describe('Auth (e2e)', () => {
  let app: INestApplication;
  let authService: AuthService;

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

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true }));
    await app.init();
    authService = app.get(AuthService);
  });

  it('registers a user and returns jwt token', async () => {
    const result = await authService.register({ username: 'tom', email: 'tom@example.com', password: 'password123' });
    expect(typeof result.accessToken).toBe('string');
  });

  it('rejects login with wrong password', async () => {
    await expect(authService.validateUser({ email: 'tom@example.com', password: 'wrong-password' }))
      .rejects.toThrow(UnauthorizedException);
  });
});

3.3 test/app.e2e-spec.ts - 模块图可用性测试

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { AppController } from '../src/app.controller';
import { AppModule } from '../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication;
  let appController: AppController;

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

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true }));
    await app.init();
    appController = app.get(AppController);
  });

  it('resolves app controller through module graph', () => {
    expect(appController.getHello()).toBe('Hello World!');
  });
});

3.4 Dockerfile - 交付一致性

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main"]

3.5 本节落地价值

你现在拥有了“可验证、可构建、可部署”的完整闭环,这是真正工程化项目的门槛线。

4. 总结这节课知识点

  • 你掌握了 Nest 的测试装配方式和 Provider Mock 思路。
  • 你把质量保障从“手工验证”升级为“自动验证”。
  • 你具备了从开发到交付的完整能力链。

5. 每节课分支

  • lesson-10-testing-delivery
  • 远程仓库:https://github.com/Shiuwn/nestjs-tutorial.git

返回课程目录

上一课:Swagger 文档、限流与缓存

下一课:日志、请求链路追踪与问题排查

— 感谢阅读 —