by 雪隐 from juejin.cn/user/143341...
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权
概述
本章将介绍在NestJS中进行端到端(e2e)测试的方法。
e2e测试介绍
在 NestJS 中,端到端(End-to-End)测试通常使用测试工具和框架,比如 SuperTest,以确保整个应用程序在真实环境中能够正常工作。根据官方文档以下是一个简要的介绍 NestJS 中如何进行端到端测试:
ts
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
import { INestApplication } from '@nestjs/common';
describe('Cats', () => {
let app: INestApplication;
// `catsService`: 用于模拟 CatsService,提供一个包含单个元素 `'test'` 的数组。
let catsService = { findAll: () => ['test'] };
beforeAll(async () => {
// 使用 NestJS 的 `Test.createTestingModule` 创建测试模块。
// 使用 `.overrideProvider` 和 `.useValue` 来替换 CatsService 的实际提供者,使用 `catsService` 替代。
// 编译测试模块,创建 NestJS 应用实例,并初始化应用。
const moduleRef = await Test.createTestingModule({
imports: [CatsModule],
})
.overrideProvider(CatsService)
.useValue(catsService)
.compile();
app = moduleRef.createNestApplication();
await app.init();
});
// 使用 Supertest 发起一个 GET 请求到 `/cats` 路径。
// 预期 HTTP 响应状态码为 200。
// 预期响应体包含 `{ data: catsService.findAll() }`。
it(`/GET cats`, () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect({
data: catsService.findAll(),
});
});
// 在所有测试完成后关闭 NestJS 应用。
afterAll(async () => {
await app.close();
});
});
在NestJS中进行e2e测试
1. 提取共同代码
根据官方的例子,我们首先需要创建测试应用。可以按照我下面的写法来初始化 测试实例:
ts
const moduleFixture: TestingModule = await Test.createTestingModule({
// 导入AppModule
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
然而,这样一来我们仅仅导入了app.module.ts
中的内容,而main.ts
中的内容并未生效。为了能在测试中重复利用这段代码,我们可以将main.ts
的内容抽离出来。
ts
// src\main.ts
async function bootstrap() {
const app = await NestFactory.create<INestApplication>(AppModule);
// 把内容提取出去
setupApp(app);
await app.listen(SERVER_VALUE.port, SERVER_VALUE.host);
}
bootstrap();
setup.ts
就是提取出来的代码。
ts
// src\setup.ts
declare const module: any;
export const setupApp = async (app: INestApplication) => {
// 统一响应体格式
app.useGlobalInterceptors(new TransformInterceptor());
// 接口版本化管理
app.enableVersioning({ type: VersioningType.URI });
// 加上全局前缀
app.setGlobalPrefix('api');
// 创建文档
generateDocument(app);
// 配置静态文件夹
// 设置静态文件目录
(app as NestExpressApplication).useStaticAssets(
join(__dirname, '..', 'public'),
);
// 热重载
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
// 全局验证管道
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
// 允许跨域
app.enableCors();
// helmet
app.use(helmet());
};
最后在测试的代码中,创建应用以后使用setup
函数并把app实例传入
ts
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
// 在这里调用
setupApp(this.app);
2. 创建测试工厂并初始化数据库
这样写虽然没有什么问题,但是在每个测试模块里面都写这段代码,感觉重复性太高了,所以干脆做成一个工厂,并且把数据连接配置也放进去,编写initDB
方法来初始化数据库,在这个方法中,我添加了一个登录用户作为基础数据。并且为了让各个测试用例不互相影响,cleanup
方法在每个测试完成以后删除所有的数据。
ts
const { SERVER_VALUE } = getConfig();
export class AppFactory {
private mongooseConnection: Connection;
private app: INestApplication;
constructor() {}
// 创建应用
async createApp() {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
this.app = moduleFixture.createNestApplication();
setupApp(this.app);
await this.app.listen(SERVER_VALUE.port, SERVER_VALUE.host);
}
// 初始化数据库连接
async initDB() {
this.mongooseConnection = this.app.get(getConnectionToken());
// 执行数据库初始化操作,插入初始数据等
const modelName = User.name;
const models = this.mongooseConnection?.models;
// 加入预先希望设定好的数据,创建测试用户
const createUserDto: CreateUserDto = {
username: 'test',
password: '123456',
status: 1,
};
const encryptedPassword = encodePassword(createUserDto.password);
const userToCreate = {
...createUserDto,
password: encryptedPassword,
};
await models[modelName].create(userToCreate);
}
// 每个测试用例测试完成之后,删除测试数据
async cleanup() {
const models = this.mongooseConnection?.models;
for (const modelName in models) {
await models[modelName].deleteMany({});
}
}
// 释放数据库连接
async destroy() {
if (this.mongooseConnection) {
this.mongooseConnection.close();
}
if (this.app) {
await this.app.close();
}
}
// 关闭应用
async close() {
if (this.app) {
await this.app.close();
}
}
}
加密的代码如下:
ts
// src/test/common.ts
import * as bcrypt from 'bcryptjs';
/**
* 密码加密
* @param password
* @returns
*/
export const encodePassword = (password: string): string => {
const salt = bcrypt.genSaltSync(10);
return bcrypt.hashSync(password, salt);
};
编写配置文件jest-e2e.json
,设置setup-jest.ts
作为全局启动文件:
json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"setupFilesAfterEnv": ["<rootDir>/setup-jest.ts"]
}
setup-jest.ts
的内容如下:
ts
import { AppFactory } from './app.factory';
import * as pactum from 'pactum';
let appFactory: AppFactory;
global.beforeEach(async () => {
appFactory = new AppFactory();
await appFactory.createApp();
await appFactory.initDB();
global.appFactory = appFactory;
});
global.afterEach(async () => {
await appFactory.cleanup();
await appFactory.close();
});
并且要修改package.json
里面的启动配置:
json
{
"scripts": {
// 设置启动配置文件为jest-e2e.json,设定最大工作线程为1。
"test:e2e": "jest --config ./test/jest-e2e.json --maxWorkers=1",
"test:e2e:watch": "jest --config ./test/jest-e2e.json --maxWorkers=1 --watch",
},
}
因为e2e的测试是并发执行的,但是我们的工厂里面有创建数据库,删除数据的操作,如果并发操作,会导致测试数据混乱,所以这里指定maxWorkers=1
,这样同一时间只有一个测试用例在执行。
3. 添加第三方工具
接下来,我们需要一个工具来发起请求,官方推荐的是superTest
,我用的是pactum
(官方文档)。大致功能都差不多,直接集成到setup-jest.ts
中去:
首先安装pactum
:
shall
pnpm i -D pactum
然后封装pactum
ts
// src/test/
// ...
global.beforeEach(async () => {
appFactory = new AppFactory();
await appFactory.createApp();
await appFactory.initDB();
// 添加下面3行
pactum.request.setBaseUrl(await appFactory.url);
global.pactum = pactum;
global.spec = pactum.spec();
global.appFactory = appFactory;
});
// ...
在app.factory.ts
中添加Url
ts
export class AppFactory {
// ...
get url() {
return this.app.getUrl();
}
// ...
}
测试User模块
准备工作都做的差不多了。让我们简单来测试下User模块。在test
文件夹中添加user.e2e-spec.ts
,并添加代码如下:
ts
import * as Spec from 'pactum/src/models/Spec';
import { getToken } from './common';
describe('UserController (e2e)', () => {
let spec: Spec;
let token: string;
beforeEach(async () => {
spec = global.spec as Spec;
// 提取token
token = await getToken(global.pactum.spec());
});
// 测试创建用户,预期结果是创建成功,并且返回结果为201,返回内容里面success为true
it('用户正常创建', async () => {
return spec
.post('/api/user/create')
.withHeaders({ Authorization: `Bearer ${token}` })
.withBody({
username: 'test1',
password: '123456',
status: 1,
})
.expectStatus(201)
.expectJsonLike({ success: true });
});
// 如果请求头中没有携带Authorization,则没有权限
it('没有创建权限', async () => {
return spec
.post('/api/user/create')
.withBody({
username: 'test1',
password: '123456',
status: 1,
})
.expectStatus(200)
.expectJsonLike({
success: false,
message: '抱歉哦,您无此权限!',
status: 10003,
});
});
it('参数错误', async () => {
return spec
.post('/api/user/create')
.withHeaders({ Authorization: `Bearer ${token}` })
.withBody({
username: 'test1',
password: '123456',
status: 3,
})
.expectStatus(400)
.expectJsonLike({
message: ['status必须是1,2'],
});
});
});
下面的代码是获取token的代码:
ts
// src/test/commont.ts
/**
* 获取token
* @param spec
* @returns
*/
export const getToken = async (spec: Spec): Promise<string> => {
// 发送登录请求以获取token
const loginResponse = await spec
.post('/api/auth/login')
.withJson({ username: 'test', password: '123456' }) // 发送用户名和密码
.expectStatus(201)
.end();
// 提取token
return loginResponse.body.response.access_token;
};
总结
上面大致是e2e的测试的内容,写的不好还请多多包涵。如果您觉得这篇文章对您有帮助,请点赞评论!🙏