NestJS实战10-单元测试

by 雪隐 from juejin.cn/user/143341...

本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权

概述

NestJS 提供了一套强大的测试工具和框架,使开发人员能够轻松地编写和运行单元测试、集成测试和端到端测试。

  1. 单元测试: NestJS 使用 Jest 作为其默认的单元测试框架。Jest 是一个功能强大的 JavaScript 测试框架,支持异步代码测试、模拟函数和自动化测试。在 NestJS 中,你可以编写针对服务、控制器、拦截器等单元的测试。

示例(使用 Jest):

ts 复制代码
describe('MyService', () => {
  it('should return "Hello, World!"', () => {
    const myService = new MyService();
    expect(myService.getHello()).toBe('Hello, World!');
  });
});
  1. 集成测试: NestJS 也支持集成测试,用于测试多个组件一起工作的能力。这包括测试控制器、服务之间的协同工作等。

示例(使用 Jest):

ts 复制代码
describe('AppController (e2e)', () => {
  it('/GET hello', () => {
    return request(app.getHttpServer())
      .get('/hello')
      .expect(200)
      .expect('Hello, World!');
  });
});
  1. 端到端测试: 对于端到端测试,你可以使用类似 Supertest 的工具,模拟 HTTP 请求并测试整个应用程序的行为。

示例(使用 Supertest):

ts 复制代码
import * as request from 'supertest';

describe('AppController (e2e)', () => {
  it('/GET hello', () => {
    return request(app.getHttpServer())
      .get('/hello')
      .expect(200)
      .expect('Hello, World!');
  });
});

总体来说,NestJS 提供了灵活且全面的测试工具,开发人员可以选择适合他们项目需求的测试级别和工具。在这一章我会着重介绍如何在Nest中进行单元测试。

什么是单元测试?

单元测试是软件开发中的一种测试方法,它专注于验证代码的最小可测试单元------通常是函数、方法或类的独立部分。单元测试的目标是确保每个单元的行为都符合预期,并且在代码发生变化时能够迅速检测到问题。

以下是单元测试的一些关键特征:

  1. 独立性: 单元测试要确保测试单元的独立性,即每个测试应该仅验证一个功能或行为。这有助于隔离问题,使得当测试失败时,容易定位和修复错误。
  2. 自动化: 单元测试通常是自动化的,这意味着测试过程可以通过脚本或测试框架自动执行,而不需要手动干预。自动化测试能够更频繁地运行,提高测试的覆盖率和反馈速度。
  3. 重复性: 单元测试应该是可重复的,即每次运行测试时都应该得到相同的结果。这有助于确保测试的可靠性和稳定性。
  4. 快速执行: 单元测试应该快速执行,以便在开发过程中频繁运行。这样可以迅速发现潜在问题,并加速开发周期。
  5. 独立于外部依赖: 单元测试应该尽可能独立于外部依赖,如数据库、网络或其他服务。这有助于减少测试的复杂性和提高可维护性。

在单元测试中,开发者编写测试代码来模拟输入,并验证输出是否符合预期。测试框架通常提供断言(assertions)来检查实际输出是否等于预期输出。如果测试失败,开发者需要检查代码并修复问题。

单元测试是保障代码质量和可维护性的重要手段之一。它有助于在开发过程中早期发现和修复问题,减少集成和系统测试阶段的错误,提高软件的整体质量。

在Nest中进行单元测试

在这里我会先拿Task模块做一个例子来进行说明。测试需要一些Jest的基本知识(因为NestJS的测试是基于Jest的),请大家先了解下Jest。另外,官方文档对单元测试有比较详细的介绍,大家可以阅读参考下。

简单介绍Jest的断言

在Jest中,断言是用来检查代码是否按预期工作的关键部分。Jest提供了一组内置的断言函数,使得编写测试用例变得简单而直观。以下是一些常见的Jest断言方法:

  1. expect()

    • expect(value) 是Jest断言的起点。通过 expect 可以创建断言链,然后使用各种 matcher 来检查值是否满足某些条件。
  2. toBe()

    • toBe(value) 用于检查两个值是否严格相等(使用===比较)。适用于检查基本数据类型等。
    scss 复制代码
    javascriptCopy code
    expect(2 + 2).toBe(4);
  3. toEqual()

    • toEqual(value) 用于检查两个对象是否深度相等。适用于检查对象、数组等复杂数据类型。
    ini 复制代码
    javascriptCopy code
    const obj1 = { a: 1, b: 2 };
    const obj2 = { b: 2, a: 1 };
    expect(obj1).toEqual(obj2);
  4. not.toBe() / not.toEqual()

    • not.toBe(value)not.toEqual(value) 用于检查值是否不等于某个值。
    scss 复制代码
    javascriptCopy code
    expect(2 + 2).not.toBe(5);
  5. toBeTruthy() / toBeFalsy()

    • toBeTruthy() 用于检查值是否为真,而 toBeFalsy() 用于检查值是否为假。
    scss 复制代码
    javascriptCopy code
    expect(true).toBeTruthy();
    expect(false).toBeFalsy();
  6. toContain()

    • toContain(item) 用于检查数组或字符串是否包含特定项。
    ini 复制代码
    javascriptCopy code
    const myArray = [1, 2, 3];
    expect(myArray).toContain(2);
  7. toHaveLength()

    • toHaveLength(value) 用于检查数组或字符串的长度是否等于指定值。
    ini 复制代码
    javascriptCopy code
    const myArray = [1, 2, 3];
    expect(myArray).toHaveLength(3);
  8. toMatch()

    • toMatch(pattern) 用于检查字符串是否匹配正则表达式。
    ini 复制代码
    javascriptCopy code
    const myString = 'Hello, World!';
    expect(myString).toMatch(/Hello/);
  9. toThrow()

    • toThrow(error) 用于检查函数是否抛出异常。
    javascript 复制代码
    javascriptCopy code
    const throwErrorFunction = () => {
      throw new Error('This is an error');
    };
    expect(throwErrorFunction).toThrow('This is an error');

这只是 Jest 断言的一小部分,Jest 提供了更多的断言方法,以适应不同的测试需求。在编写测试用例时,你可以根据需要选择合适的断言方法。让我们来接着讨论如何进行单元测试。

首先安装必要的依赖

shall 复制代码
$ pnpm i --save-dev @nestjs/testing

创建测试文件

在Task模块中,新建一个__test__文件夹用于存放测试文件。测试文件需要以.spec.ts结尾,这样Nest会自动扫描所有以.spec.ts结尾的文件并进行测试。

Controller层测试示例

1. TasksController单元测试:

以下是一个示例测试代码,用于对TasksController进行单元测试:

ts 复制代码
// 导入所需模块和依赖
import { Test, TestingModule } from '@nestjs/testing';
// 其他导入语句...

describe('TasksController (当日任务模块测试)', () => {
  let controller: TasksController;
  let mockTasksService: Partial<TasksService>;

  beforeEach(async () => {
    // 模拟TasksService的行为
    mockTasksService = {
      // 模拟各个方法的行为...
    };

    // 创建测试模块
    const module: TestingModule = await Test.createTestingModule({
      controllers: [TasksController],
      providers: [
        {
          provide: TasksService,
          useValue: mockTasksService,
        },
      ],
    }).compile();

    // 获取测试模块中的控制器实例
    controller = module.get<TasksController>(TasksController);
  });

  // 各个测试用例...
});

2. 模拟各个方法的行为

首先解释下为什么要模拟方法,单元的着重测试的是测试每个单元的本身的逻辑,至于在这个单元里面调用的服务也好,NestJS提供的装饰也好(这些都是他们各自自己的单元测试应该负责的内容),都不是我们的测试目标。所以为了能够顺利通过测试,我们需要模拟TasksService的行为,并且通过providersuseValue的方式引入。

例如:

ts 复制代码
    mockTasksService = {
      create: (createTaskDto: CreateTaskDto) => {
        const task = new Task();
        task.content = createTaskDto.content;
        task.isCompleted = createTaskDto.isCompleted;
        task.title = createTaskDto.title;
        return Promise.resolve(task);
      },
    }

这里我模拟了TasksService中的create方法,直接就把createTaskDto的内容赋值给task并返回。

测试内容如下,这里我们可以确认到tasksServicecreate方法是否被调用,返回的结果是否就是tasksService的返回结果,这就足够了。

ts 复制代码
  it('创建任务', async () => {
    const res = await controller.create({
      title: 'testTitle',
      content: 'testContent',
      isCompleted: false,
    });
    // 断言确定是否有返回结果
    expect(res).not.toBeNull();
    // 断言确定返回结果的标题是否为'testTitle'
    expect(res.title).toBe('testTitle');
  });

由于controller层的内容比较简单,所以测试也比较简单,但是,TaskService中的测试就要复杂一些,并且测试逻辑和方法都会多一些。如果还是每个方法都手写模拟难免工作量有些太大了。接下来请看Service测试介绍另一种方法。

Service层测试示例

1. 简单介绍jest.fn方法

jest.fn() 是 Jest 测试框架中的一个功能,用于创建一个新的 mock 函数。Mock 函数是模拟的函数,可以用于替代实际函数的调用,以便在测试中进行跟踪、验证和控制函数的行为。

使用 jest.fn() 的基本形式
ini 复制代码
javascriptCopy code
const mockFunction = jest.fn();

通过上述代码,你创建了一个名为 mockFunction 的 mock 函数。

常见用法和特性
  1. 捕获函数调用

    scss 复制代码
    javascriptCopy code
    mockFunction();
    expect(mockFunction).toHaveBeenCalled();

    使用 toHaveBeenCalled 来验证函数是否被调用过。

  2. 自定义 mock 函数的返回值

    scss 复制代码
    javascriptCopy code
    mockFunction.mockReturnValue(42);
    const result = mockFunction();
    expect(result).toBe(42);

    使用 mockReturnValue 来定义 mock 函数的返回值。

  3. 定制 mock 函数的实现

    scss 复制代码
    javascriptCopy code
    mockFunction.mockImplementation(() => 'custom implementation');
    const result = mockFunction();
    expect(result).toBe('custom implementation');

    使用 mockImplementation 来指定 mock 函数的具体实现。

  4. 跟踪函数调用参数

    arduino 复制代码
    javascriptCopy code
    mockFunction('arg1', 'arg2');
    expect(mockFunction).toHaveBeenCalledWith('arg1', 'arg2');

    使用 toHaveBeenCalledWith 来验证函数是否带有特定的参数调用。

  5. 清除 mock 函数的状态

    scss 复制代码
    javascriptCopy code
    mockFunction.mockClear();
    expect(mockFunction).not.toHaveBeenCalled();

    使用 mockClear 来清除 mock 函数的调用状态,以便在下一个测试中重新开始。

jest.fn() 是 Jest 中强大的 mock 函数创建工具之一,它使测试更容易,可以对函数的行为进行灵活的控制和验证。在单元测试中,通过使用 mock 函数,你可以更好地隔离和测试代码的各个部分。

2. TasksService单元测试:

以下是一个示例测试代码,用于对TasksService进行单元测试:

ts 复制代码
// 导入所需模块和依赖
import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';
// 其他导入语句...

describe('TestService (当日任务服务模块)', () => {
  let service: TasksService;
  let mockTaskModel: Partial<Model<Task>> & {
    create: jest.Mock;
    find: jest.Mock;
    findById: jest.Mock;
    findByIdAndUpdate: jest.Mock;
    deleteOne: jest.Mock;
    countDocuments: jest.Mock;
    deleteMany: jest.Mock;
    aggregate: jest.Mock;
  };

  beforeEach(async () => {
    // 模拟TasksService的行为
    mockTaskModel = {
      create: jest.fn(),
      find: jest.fn(),
      findById: jest.fn(),
      findByIdAndUpdate: jest.fn(),
      deleteOne: jest.fn(),
      countDocuments: jest.fn(),
      deleteMany: jest.fn(),
      aggregate: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TasksService,
        {
          // 注意这里的要使用getModelToken
          provide: getModelToken(Task.name),
          useValue: mockTaskModel,
        },
      ],
    }).compile();
    service = module.get<TasksService>(TasksService);
  });

  // 各个测试用例...
});

在这个测试中使用了jest.fn()方法,他可以模拟返回结果方便我们测试。

测试例:

ts 复制代码
 describe('当日任务 create', () => {
    it('当日任务创建成功', async () => {
      const createdTask = new Task();
      mockTaskModel.create.mockResolvedValue(createdTask);

      const createTaskDto = new CreateTaskDto();
      const result = await service.create(createTaskDto);

      expect(mockTaskModel.create).toHaveBeenCalledWith(createTaskDto);

      expect(result).toEqual(createdTask);
    });

    it('当日任务创建失败', async () => {
      mockTaskModel.create.mockResolvedValue(undefined);

      const createTaskDto = new CreateTaskDto();

      try {
        await service.create(createTaskDto);
        fail('期望抛出BusinessException,但是没有');
      } catch (error) {
        expect(error).toBeInstanceOf(BusinessException);
        expect(error.message).toBe('当日任务创建失败');
      }
    });
  }); describe('当日任务 create', () => {
    it('当日任务创建成功', async () => {
      const createdTask = new Task();
      mockTaskModel.create.mockResolvedValue(createdTask);

      const createTaskDto = new CreateTaskDto();
      const result = await service.create(createTaskDto);

      expect(mockTaskModel.create).toHaveBeenCalledWith(createTaskDto);

      expect(result).toEqual(createdTask);
    });

    it('当日任务创建失败', async () => {
      mockTaskModel.create.mockResolvedValue(undefined);

      const createTaskDto = new CreateTaskDto();

      try {
        await service.create(createTaskDto);
        fail('期望抛出BusinessException,但是没有');
      } catch (error) {
        expect(error).toBeInstanceOf(BusinessException);
        expect(error.message).toBe('当日任务创建失败');
      }
    });
  });

运行测试

1. 在package.json中有提供了下面这些单元测试命令, 让我来简单介绍下。

json 复制代码
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
  1. "test": "jest"

    • 这是运行基本测试的命令。
    • jest 命令用于执行测试套件。当你运行 npm test 时,将执行这个命令。
    • 例如,可以使用 npm testyarn test 来运行所有测试。
  2. "test:watch": "jest --watch"

    • 这是一个用于监视文件更改并自动运行相关测试的命令。
    • --watch 标志启动 Jest 的监视模式,它会持续监听文件的变化,并在文件更改时自动运行相关的测试。
    • 适用于开发过程中,当你保存文件时自动运行相关测试。
  3. "test:cov": "jest --coverage"

    • 这是一个用于生成测试覆盖率报告的命令。
    • --coverage 标志告诉 Jest 在测试运行后生成代码覆盖率报告。
    • 运行此命令后,你可以在生成的 coverage 目录下找到详细的测试覆盖率报告。
  4. "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"

    • 这是一个用于在调试模式下运行测试的命令。
    • --inspect-brk 使 Node.js 在启动时进入调试模式并在第一行暂停。
    • -r tsconfig-paths/register -r ts-node/register 用于在运行 Jest 之前注册 TypeScript 的路径和 Node.js 环境。
    • --runInBand 用于强制 Jest 以单线程模式运行,以便更容易调试。
    • 在调试模式下,你可以使用 Node.js 调试器连接到运行的 Jest 进程以进行调试。

2.运行test 命令

让我简单运行下测试的命令:

shall 复制代码
npm run test

运行结果如下:

上面的结果表示测试全部通过,如果有不通过的测试会以错误的方式呈现出来。

3.运行test:cov 命令

这个命令主要是为了查看覆盖率,看看自己有没有漏测试的点。

运行结果如下:

除了module层,其他的内容都是100%。说明没有漏测的点,这样整个单元测试的内容就介绍的差不多了。

总结

更加具体的内容大家可以看本章代码,慢慢体会。如果有什么不懂的地方请在评论区留言。如果大家觉得这篇文章对您们有帮助,请点赞和评论🙏!

本章代码

代码

相关推荐
程序员黑豆10 分钟前
Java包装类:基本类型与对象的桥梁
java·前端·ai编程
bugcome_com3 小时前
JWT 知识小课堂:从入门到精通,一文吃透 JSON Web Token
前端·json
To_OC3 小时前
写了三天 Next.js,我的 React 世界观被拆了又装回去
前端·全栈·next.js
独泪了无痕3 小时前
viewer.js 安装与配置指南:实现图片预览功能
前端·vue.js
用户938515635074 小时前
工厂模式与 Nest.js 核心思想 —— 从蜜雪冰城到企业级架构
后端·设计模式·nestjs
用户938515635074 小时前
实战 Todo CRUD —— 从路由到异常,手写一个完整模块
后端·typescript·nestjs
起床学FPGA5 小时前
AI回答问题之后,会自动跳到最下面的问题
开发语言·javascript·ecmascript
Fluxart.ai5 小时前
电商商品图审核怎么自动化?规则引擎、人工复核与发布门禁
java·前端·自动化
why技术6 小时前
AI 写的文章,可能都带着手敲一遍都去不掉的“隐形水印”。
前端·人工智能·后端
愚公搬代码6 小时前
【愚公系列】《Android应用案例开发大全》016-LBS类应用掌上杭州(辅助工具类的开发)
android·前端