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

本章代码

代码

相关推荐
To_OC8 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
To_OC8 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic8 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy8 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志9 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble10 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长10 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯11 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜11 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司12 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能