by 雪隐 from juejin.cn/user/143341...
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权
概述
NestJS 提供了一套强大的测试工具和框架,使开发人员能够轻松地编写和运行单元测试、集成测试和端到端测试。
- 单元测试: NestJS 使用 Jest 作为其默认的单元测试框架。Jest 是一个功能强大的 JavaScript 测试框架,支持异步代码测试、模拟函数和自动化测试。在 NestJS 中,你可以编写针对服务、控制器、拦截器等单元的测试。
示例(使用 Jest):
ts
describe('MyService', () => {
it('should return "Hello, World!"', () => {
const myService = new MyService();
expect(myService.getHello()).toBe('Hello, World!');
});
});
- 集成测试: NestJS 也支持集成测试,用于测试多个组件一起工作的能力。这包括测试控制器、服务之间的协同工作等。
示例(使用 Jest):
ts
describe('AppController (e2e)', () => {
it('/GET hello', () => {
return request(app.getHttpServer())
.get('/hello')
.expect(200)
.expect('Hello, World!');
});
});
- 端到端测试: 对于端到端测试,你可以使用类似 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
中进行单元测试。
什么是单元测试?
单元测试是软件开发中的一种测试方法,它专注于验证代码的最小可测试单元------通常是函数、方法或类的独立部分。单元测试的目标是确保每个单元的行为都符合预期,并且在代码发生变化时能够迅速检测到问题。
以下是单元测试的一些关键特征:
- 独立性: 单元测试要确保测试单元的独立性,即每个测试应该仅验证一个功能或行为。这有助于隔离问题,使得当测试失败时,容易定位和修复错误。
- 自动化: 单元测试通常是自动化的,这意味着测试过程可以通过脚本或测试框架自动执行,而不需要手动干预。自动化测试能够更频繁地运行,提高测试的覆盖率和反馈速度。
- 重复性: 单元测试应该是可重复的,即每次运行测试时都应该得到相同的结果。这有助于确保测试的可靠性和稳定性。
- 快速执行: 单元测试应该快速执行,以便在开发过程中频繁运行。这样可以迅速发现潜在问题,并加速开发周期。
- 独立于外部依赖: 单元测试应该尽可能独立于外部依赖,如数据库、网络或其他服务。这有助于减少测试的复杂性和提高可维护性。
在单元测试中,开发者编写测试代码来模拟输入,并验证输出是否符合预期。测试框架通常提供断言(assertions)来检查实际输出是否等于预期输出。如果测试失败,开发者需要检查代码并修复问题。
单元测试是保障代码质量和可维护性的重要手段之一。它有助于在开发过程中早期发现和修复问题,减少集成和系统测试阶段的错误,提高软件的整体质量。
在Nest中进行单元测试
在这里我会先拿Task模块做一个例子来进行说明。测试需要一些Jest的基本知识(因为NestJS的测试是基于Jest的),请大家先了解下Jest。另外,官方文档对单元测试有比较详细的介绍,大家可以阅读参考下。
简单介绍Jest的断言
在Jest中,断言是用来检查代码是否按预期工作的关键部分。Jest提供了一组内置的断言函数,使得编写测试用例变得简单而直观。以下是一些常见的Jest断言方法:
-
expect()
expect(value)
是Jest断言的起点。通过expect
可以创建断言链,然后使用各种 matcher 来检查值是否满足某些条件。
-
toBe()
toBe(value)
用于检查两个值是否严格相等(使用===
比较)。适用于检查基本数据类型等。
scssjavascriptCopy code expect(2 + 2).toBe(4);
-
toEqual()
toEqual(value)
用于检查两个对象是否深度相等。适用于检查对象、数组等复杂数据类型。
inijavascriptCopy code const obj1 = { a: 1, b: 2 }; const obj2 = { b: 2, a: 1 }; expect(obj1).toEqual(obj2);
-
not.toBe() / not.toEqual()
not.toBe(value)
和not.toEqual(value)
用于检查值是否不等于某个值。
scssjavascriptCopy code expect(2 + 2).not.toBe(5);
-
toBeTruthy() / toBeFalsy()
toBeTruthy()
用于检查值是否为真,而toBeFalsy()
用于检查值是否为假。
scssjavascriptCopy code expect(true).toBeTruthy(); expect(false).toBeFalsy();
-
toContain()
toContain(item)
用于检查数组或字符串是否包含特定项。
inijavascriptCopy code const myArray = [1, 2, 3]; expect(myArray).toContain(2);
-
toHaveLength()
toHaveLength(value)
用于检查数组或字符串的长度是否等于指定值。
inijavascriptCopy code const myArray = [1, 2, 3]; expect(myArray).toHaveLength(3);
-
toMatch()
toMatch(pattern)
用于检查字符串是否匹配正则表达式。
inijavascriptCopy code const myString = 'Hello, World!'; expect(myString).toMatch(/Hello/);
-
toThrow()
toThrow(error)
用于检查函数是否抛出异常。
javascriptjavascriptCopy 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
的行为,并且通过providers
的useValue
的方式引入。
例如:
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
并返回。
测试内容如下,这里我们可以确认到tasksService
的create
方法是否被调用,返回的结果是否就是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 函数。
常见用法和特性
-
捕获函数调用
scssjavascriptCopy code mockFunction(); expect(mockFunction).toHaveBeenCalled();
使用
toHaveBeenCalled
来验证函数是否被调用过。 -
自定义 mock 函数的返回值
scssjavascriptCopy code mockFunction.mockReturnValue(42); const result = mockFunction(); expect(result).toBe(42);
使用
mockReturnValue
来定义 mock 函数的返回值。 -
定制 mock 函数的实现
scssjavascriptCopy code mockFunction.mockImplementation(() => 'custom implementation'); const result = mockFunction(); expect(result).toBe('custom implementation');
使用
mockImplementation
来指定 mock 函数的具体实现。 -
跟踪函数调用参数
arduinojavascriptCopy code mockFunction('arg1', 'arg2'); expect(mockFunction).toHaveBeenCalledWith('arg1', 'arg2');
使用
toHaveBeenCalledWith
来验证函数是否带有特定的参数调用。 -
清除 mock 函数的状态
scssjavascriptCopy 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",
-
"test": "jest"
- 这是运行基本测试的命令。
jest
命令用于执行测试套件。当你运行npm test
时,将执行这个命令。- 例如,可以使用
npm test
或yarn test
来运行所有测试。
-
"test:watch": "jest --watch"
- 这是一个用于监视文件更改并自动运行相关测试的命令。
--watch
标志启动 Jest 的监视模式,它会持续监听文件的变化,并在文件更改时自动运行相关的测试。- 适用于开发过程中,当你保存文件时自动运行相关测试。
-
"test:cov": "jest --coverage"
- 这是一个用于生成测试覆盖率报告的命令。
--coverage
标志告诉 Jest 在测试运行后生成代码覆盖率报告。- 运行此命令后,你可以在生成的
coverage
目录下找到详细的测试覆盖率报告。
-
"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%。说明没有漏测的点,这样整个单元测试的内容就介绍的差不多了。
总结
更加具体的内容大家可以看本章代码,慢慢体会。如果有什么不懂的地方请在评论区留言。如果大家觉得这篇文章对您们有帮助,请点赞和评论🙏!