NestJS实战11-e2e测试

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的测试的内容,写的不好还请多多包涵。如果您觉得这篇文章对您有帮助,请点赞评论!🙏

本章代码

代码

相关推荐
莹雨潇潇2 分钟前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr10 分钟前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho1 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ2 小时前
html+css+js实现step进度条效果
javascript·css·html
小白学习日记3 小时前
【复习】HTML常用标签<table>
前端·html
john_hjy3 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd3 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java3 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js