Nest 的官方 demo 中提供了两套测试:

  • Unit Test
  • E2E Test

一个业务要写 E2E Test 还是 Unit Test 要根据实际情况来分析。 建议:

  • 主流程(成功/失败):E2E Test
  • 异常 case : Unit Test

接下来和大家详细解释一下两套测试

# Unit Test

即是单元测试,测试粒度非常小,一般是测试一个 function,可以测很多种 case, 代价 function 的外部依赖都需要 mock 掉,如其他 function 调用数据查询等。

这里我们将会参考 Nest 的风格:

  • Unit Test 文件位置和代码同个位置,以后缀区分
  • 测试粒度是 function
  • 测试不会连接 mongodb

如果你发现测试一个 service function 的时候,不连着 mongodb 就不好写测试的时候,就要考虑修改一下代码结构了,把数据查询的逻辑独立到另一个方法去。 毕竟业界是有 “编写可测试的代码” 这个说法的。

执行测试

$ npm run test

# E2E Test

端到端测试,测试粒度比较大,模拟请求接口,然后检查返回值,好处是测试覆盖的流程多, 用比较低的成本就可以获得 80% 以上的覆盖率,坏处是不够灵活。

而且为了能覆盖到数据查询的流程,我们的习惯是连着 mongodb 跑的, 这里我们就不参考 Nest 的风格了。

E2E Test 依赖 mongodb,所以开始之前请修改 config/test.js 里的 mongodb uri

执行测试

$ npm run test:e2e

# 准备测试数据

根据我们之前的经验,我们发现 mock mongodb db 的成本比较高,不如直接连着 mongodb 测试。

但是这种做法要求测试之前我们要准备好的基础数据,这个基础数据有个专有名词,叫 Fixtures。

在之前的开发经验中,我们会选择新建一个文件来保存 Fixtures。例如测试文件是: test/users/users-find.e2e-spec.ts 则新建一个 test/users/users-find.e2e-spec.data.ts 里面保存每个 Model 的数据,在测试开始之前,data 会被写入 DB,测试结束的时候清理掉。

不过现在使用了 jest 测试框架,因为 jest 不会暴露运行时状态,所以无法继续使用上述方式准备 Fixtures 了, 只能在每个测试文件开头处,声明 Fixtures。

describe('AppController (e2e) find with fixtures ', () => {
  beforeAll(async function () {
    await genFixtures(UserTemplate, 10, 'User')
  })

  it('find all', () => {
    request
      .get('/users')
      .expect(200)
  })
})

声明 await genFixtures(UserTemplate, 10, 'User') 之后,数据库就会生成 10 个用户数据.

UserTemplate 长这样:

export const UserTemplate = {
  _id: () => ObjectId(),
  'name': () => Mock.Random.first(),
  phone: /1\d{10}/
}

这里依赖了 mockjs 帮忙把 UserTemplate 模板生成实际数据,再通过我们封装好的 genFixtures 方法把数据写入 DB 中

如果你需要生成多个表的数据,并且还需要指定数据之间的关联关系,可以参考 test/users/users-multi-db.e2e-spec.ts 这个测试的实现:

test/users/users-multi-db.e2e-spec.ts

beforeAll(async function () {
    // 自动生成 User fixture
    let userData = await genFixtures(UserTemplate, 1, 'User')
    user = userData[0]
    userId = user._id.toString()

    // 自动生成 Account fixture,并且通过 fixData 函数来修改值
    await genFixtures(AccountTemplate, 1, 'Account', (it: any) => {
      it.userId = userId
      return it
    })
  })

生成 Account 的时候修改一下 UserId

# Mock Service

如果测试中需要 mock service 方法,可以参考

sample/hello-world/test/users/users-register.e2e-spec.ts

it('register with service mock', async () => {
    let usersService = testModule.get<UsersService>(UsersService)
    let spy = jest.spyOn(usersService, 'register').mockImplementation(async () => ({mock: true} as any))
    let {body} = await request
      .post('/users/register')
      .send({name: 'nick', phone: '12345'})
      .expect(201)
      .expect({
        'code': 0,
        'message': 'success',
        'data': {
          'mock': true
        }
      })
    console.log('body', body)
    expect(spy).toHaveBeenCalled()
    // 一定要 restore 不然会影响其他测试
    spy.mockRestore()
})

因为 nestjs 里依赖注入的对象是单例的,我们从容器里取出 usersService 后, 使用 jest.spyOn 监听和修改 register 方法,是可以全局生效的。

注意记得在测试结束的时候 spy.mockRestore()

# 无法快速运行测试的问题

脚手架里实际存在了两套测试,所以 e2e 测试无法继续享受 WebStorm 点击快速运行测试的便利了。

这里点击测试的话,会得到如下结果:

No tests found, exiting with code 1

Run with --passWithNoTests to exit with code 0

这是因为 Webstorm 默认读取的是 package.json 中 jest 的配置信息,但是这个测试是 unit test 的,所以会找不到测试文件。

如果需要快速 run test 这个功能, 可以考虑配置一下 jest 的 run 模板 把 --config ./test/jest-e2e.json 作为固定参数写入模板中。