应用测试

经过大量的实践,我们沉淀出了一套标准的测试工具集。

测试目录结构

我们约定 test 目录为存放所有测试脚本的目录,测试所使用到的 fixtures 和相关辅助脚本都应该放在此目录下。

测试脚本文件统一按 ${filename}.test.ts 命名,必须以 .test.ts 作为文件后缀。

一个应用的测试目录示例:

  1. test
  2. ├── controller
  3. └── home.test.ts
  4. ├── hello.test.ts
  5. └── service
  6. └── user.test.ts

测试命令

在脚手架中,我们已经将常见的命令进行内置,可能略微有些不同,大致代码如下。

  1. {
  2. "scripts": {
  3. "test": "midway-bin test --ts",
  4. "cov": "midway-bin cov --ts",
  5. }
  6. }

然后就可以按标准的 npm test来运行测试了。

  1. npm test
  2. > unittest-example@ test /Users/harry/midwayj/examples/unittest
  3. > midway-bin test --ts
  4. test/hello.test.ts
  5. should work
  6. 1 passing (10ms)

开始测试

在测试运行之前,我们首先要创建应用的一个 app 实例, 通过它来访问需要被测试的 Controller、Middleware、Service 等应用层代码。

通过 midway-mock,结合 Mocha 的 before 钩子就可以便捷地创建出一个 app 实例。

  1. // test/controller/home.test.js
  2. const assert = require('assert');
  3. import { mm } from 'midway-mock';
  4. describe('test/controller/home.test.ts', () => {
  5. let app;
  6. before(() => {
  7. // 创建当前应用的 app 实例
  8. app = mm.app();
  9. // 等待 app 启动成功,才能执行测试用例
  10. return app.ready();
  11. });
  12. });

这样我们就拿到了一个 app 的引用,接下来所有测试用例都会基于这个 app 进行。 更多关于创建 app 的信息请查看 mm.app(options) 文档。

每一个测试文件都需要这样创建一个 app 实例非常冗余,因此 midway-mock 提供了一个 bootstrap 文件,可以直接从它上面拿到我们所常用的实例:

  1. // test/controller/home.test.ts
  2. import { app, mock, assert } from 'midway-mock/bootstrap';
  3. describe('test/controller/home.test.ts', () => {
  4. // test cases
  5. });

Controller 测试

我们可以通过 app.httpRequest() SuperTest 发起一个真实的 HTTP 请求来进行测试。app.httpRequest() 是 midway-mock 封装的 SuperTest 请求实例。

例如我们要给 app/controller/home.ts

  1. import { controller, get, provide } from 'midway';
  2. @provide()
  3. @controller('/')
  4. export class HomeController {
  5. @get('/')
  6. async index(ctx) {
  7. ctx.body = `Welcome to midwayjs!`;
  8. }
  9. }

写一个完整的单元测试,它的测试代码 test/controller/home.test.ts 如下:

  1. import { app, assert } from 'midway-mock/bootstrap';
  2. describe('test/controller/home.test.ts', () => {
  3. it('should GET /', () => {
  4. // 对 app 发起 `GET /` 请求
  5. return app
  6. .httpRequest()
  7. .get('/')
  8. .expect('Welcome to midwayjs!') // 期望 body 是 hello world
  9. .expect(200); // 期望返回 status 200
  10. });
  11. });

通过基于 SuperTest 的 app.httpRequest() 可以轻松发起 GET、POST、PUT 等 HTTP 请求,并且它有非常丰富的请求数据构造接口,请查看 SuperTest 文档。

Service 测试

由于 midway 提倡使用 IoC 的方式来定义 service,所以编码与测试都与 eggjs 有明显的区别。

例如 src/lib/service/user.ts:

  1. import { provide } from 'midway';
  2. import { IUserService, IUserOptions, IUserResult } from '../../interface';
  3. // 装载 service 到 IoC 容器
  4. @provide('userService')
  5. export class UserService implements IUserService {
  6. async getUser(options: IUserOptions): Promise<IUserResult> {
  7. return new Promise<IUserResult>((resolve) => {
  8. // 10ms 之后返回用户数据
  9. setTimeout(() => {
  10. resolve({
  11. id: options.id,
  12. username: 'mockedName',
  13. phone: '12345678901',
  14. email: 'xxx.xxx@xxx.com',
  15. });
  16. }, 10);
  17. });
  18. }
  19. }

编写单元测试 test/service/user.test.ts

  1. import { app, assert } from 'midway-mock/bootstrap';
  2. import { IUserService } from '../../src/interface';
  3. describe('test/service/user.test.ts', () => {
  4. it('#getUser', async () => {
  5. // 取出 userService
  6. const user = await app.applicationContext.getAsync<IUserService>('userService');
  7. const data = await user.getUser({ id: 1 });
  8. assert(data.id === 1);
  9. assert(data.username === 'mockedName');
  10. });
  11. });

app.applicationContext 是 IoC 容器的应用上下文, 通过它可以异步取出注入的 service,并使用 service 进行测试。完整 demo 可以参见 midway-test-demo