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%。说明没有漏测的点,这样整个单元测试的内容就介绍的差不多了。

总结

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

本章代码

代码

相关推荐
一只欢喜29 分钟前
uniapp使用uview2上传图片功能
前端·uni-app
尸僵打怪兽42 分钟前
后台数据管理系统 - 项目架构设计-Vue3+axios+Element-plus(0920)
前端·javascript·vue.js·elementui·axios·博客·后台管理系统
ggome1 小时前
Uniapp低版本的安卓不能用解决办法
前端·javascript·uni-app
Ylucius1 小时前
JavaScript 与 Java 的继承有何区别?-----原型继承,单继承有何联系?
java·开发语言·前端·javascript·后端·学习
前端初见1 小时前
双token无感刷新
前端·javascript
、昔年1 小时前
前端univer创建、编辑excel
前端·excel·univer
emmm4591 小时前
前端中常见的三种存储方式Cookie、localStorage 和 sessionStorage。
前端
Q186000000001 小时前
在HTML中添加视频
前端·html·音视频
bin91531 小时前
前端JavaScript导出excel,并用excel分析数据,使用SheetJS导出excel
前端·javascript·excel
Rattenking2 小时前
node - npm常用命令和package.json说明
前端·npm·json