【Nest】单元测试

一、为什么要写单元测试

  • 保持业务逻辑稳定,防回归。
  • 提高重构信心。
  • 文档化接口/行为(测试即规格)。
  • 更快定位 bug(单元测试通常比手动或集成测试更细粒度、更快)。

二、Nest 单元测试的基本思路

  1. 只测试单元(一个类 / 函数):不要启动真实服务器、不要访问真实数据库或第三方服务(因此需要使用 mock 数据),我们只是测试代码逻辑,不测试数据库操作 / 传参(class-transformer...) 等等。
  2. 使用依赖注入(DI)+ mock :通过 Nest 的 Test.createTestingModule 创建模块,把依赖替换成 mock(手工或 jest mock)。
  3. 断言行为而非实现:关注方法返回、调用了哪些依赖、抛出哪些错误等。
  4. 异步处理要正确 await :测试 async 函数或返回 Promise 的方法时必须 await 或使用 Jest 的 resolves/rejects

三、常用工具与配置(快速)

  • 测试框架:Jest@nestjs/testingts-jest
  • Mock:Jest 的 jest.fn()jest.spyOn() 或手动提供假的对象
  • 运行脚本(package.json):
json 复制代码
"scripts": {
  "test": "jest",
  "test:watch": "jest --watch",
  "test:cov": "jest --coverage"
}

四、示例代码

假设有一个简单业务:UsersService 提供 findById,内部依赖 UserRepository(模拟 TypeORM repository)。

目录:

复制代码
src/
  users/
    users.service.ts
    users.controller.ts
    dto/
    guards/
test/
  users.service.spec.ts
  users.controller.spec.ts

users.service.ts

ts 复制代码
// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor(private readonly userRepo: any /* 实际为 Repository<User> */) {}

  async findById(id: number) {
    const user = await this.userRepo.findOne({ where: { id } });
    if (!user) throw new NotFoundException(`User ${id} not found`);
    return user;
  }

  async create(dto: any) {
    return this.userRepo.save(dto);
  }
}

users.service.spec.ts(单元测试:mock repository)

ts 复制代码
// test/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from '../src/users/users.service';
import { NotFoundException } from '@nestjs/common';

describe('UsersService', () => {
  let service: UsersService;
  // 手工创建 repo mock
  const mockRepo = {
    findOne: jest.fn(),
    save: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: 'UserRepository', useValue: mockRepo }, // 以 token 注入时用这种方式
      ],
    })
      // 如果是 constructor 注入具体 token,需要手动替换 providers 的注入,下面示例假设 UsersService 接收 repo 作为第一参数
      .compile();

    // 直接 new 或者 get 如果服务构造签名匹配
    service = new UsersService(mockRepo);
    jest.clearAllMocks();
  });

  it('findById - 返回用户', async () => {
    const fake = { id: 1, name: 'Alice' };
    mockRepo.findOne.mockResolvedValue(fake);

    const res = await service.findById(1);
    expect(res).toEqual(fake);
    expect(mockRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
  });

  it('findById - 找不到抛 404', async () => {
    mockRepo.findOne.mockResolvedValue(null);
    await expect(service.findById(999)).rejects.toThrow(NotFoundException);
  });

  it('create - 调用 save 并返回', async () => {
    const dto = { name: 'Bob' };
    mockRepo.save.mockResolvedValue({ id: 2, ...dto });
    const res = await service.create(dto);
    expect(res).toEqual({ id: 2, name: 'Bob' });
    expect(mockRepo.save).toHaveBeenCalledWith(dto);
  });
});

要点说明

  • 没有启动 Nest 应用或数据库。
  • mockRepo 使用 jest.fn() 可精确断言调用参数与次数。
  • 测试关注返回值与错误抛出。

users.controller.ts

ts 复制代码
// src/users/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
  async getUser(@Param('id') id: string) {
    return this.usersService.findById(Number(id));
  }
}

users.controller.spec.ts(Controller 的单元测试:mock Service)

ts 复制代码
// test/users.controller.spec.ts
import { UsersController } from '../src/users/users.controller';
import { UsersService } from '../src/users/users.service';

describe('UsersController', () => {
  let controller: UsersController;
  const mockService = { findById: jest.fn() };

  beforeEach(() => {
    controller = new UsersController(mockService as any);
    jest.clearAllMocks();
  });

  it('getUser - 返回调用结果', async () => {
    mockService.findById.mockResolvedValue({ id: 1, name: 'Alice' });
    const res = await controller.getUser('1');
    expect(res).toEqual({ id: 1, name: 'Alice' });
    expect(mockService.findById).toHaveBeenCalledWith(1);
  });
});

要点

  • Controller 测试仅关心路由层调用 service 的行为,不测试 service 的实现(分离关注点)。

测试 Guard / Pipe 的示例

Guard、Pipe、Interceptor 都是可以单独实例化并直接调用对应方法测试的。例如测试一个简单 AuthGuard

ts 复制代码
// test/auth.guard.spec.ts
import { ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '../src/auth/auth.guard';

describe('AuthGuard', () => {
  let guard: AuthGuard;
  beforeEach(() => guard = new AuthGuard());

  it('canActivate - 允许已登录用户', async () => {
    const ctx = {
      switchToHttp: () => ({
        getRequest: () => ({ user: { id: 1 } }),
      }),
    } as unknown as ExecutionContext;
    await expect(guard.canActivate(ctx)).resolves.toBe(true);
  });

  it('canActivate - 拒绝无 user 的请求', async () => {
    const ctx = {
      switchToHttp: () => ({ getRequest: () => ({}) }),
    } as unknown as ExecutionContext;
    await expect(guard.canActivate(ctx)).resolves.toBe(false);
  });
});
相关推荐
sunnyday04261 天前
深入理解Java日志框架:Logback与Log4j2配置对比分析
java·log4j·logback
亓才孓1 天前
JUnit--Before,After,Test标签
java·junit·log4j
叶落无痕522 天前
Electron应用自动化测试实例
前端·javascript·功能测试·测试工具·electron·单元测试
汽车仪器仪表相关领域2 天前
工况模拟精准检测,合规减排赋能行业 ——NHASM-1 型稳态工况法汽车排气检测系统项目实战经验分享
数据库·算法·单元测试·汽车·压力测试·可用性测试
码农水水2 天前
大疆Java面试被问:TCC事务的悬挂、空回滚问题解决方案
java·开发语言·人工智能·面试·职场和发展·单元测试·php
卓码软件测评2 天前
CMA-CNAS软件测评报告机构【Apifox动态Mock响应处理复杂业务逻辑设计】
测试工具·性能优化·单元测试·测试用例
孙琦Ray2 天前
GitHub开源项目日报 · 2026年1月7日 · 本期热门开源全景
单元测试·开源·前端调试·浏览器自动化·知识管理·ai代理·跨语言序列化
程序员三藏2 天前
单元测试详解
自动化测试·软件测试·python·测试工具·职场和发展·单元测试·测试用例
卓码软件测评3 天前
CMA/CNAS双资质软件测评机构【Apifox高效编写自动化测试用例的技巧和规范】
测试工具·ci/cd·性能优化·单元测试·测试用例
while(1){yan}3 天前
图书管理系统(超详细版)
spring boot·spring·java-ee·tomcat·log4j·maven·mybatis