使用mocha编写node服务单元测试

2020-06-27 23:56:32 浏览数 (1)

mocha介绍

mocha作为最流行的JavaScript测试框架之一,可以用于测试node.js服务和运行在浏览器环境下的js代码。

Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun.

官方给它自己定义的三个特点是simple、flexible、fun。

flexible是因为mocha本身不包含断言库、mock等功能,用户可以根据自己的需要灵活地选择所需要的额外功能。

单元测试流程

编写单元测试代码的流程基本就是 梳理代码流程 -> 针对每一个分支编写单元测试 -> 运行单测代码 -> 查看测试覆盖率报告。

mocha本身十分简单,只要执行 mocha 命令就会默认运行test子目录下的测试脚本。但这样简单的功能并不能满足我们的需求,我们需要引入一些npm包来加强一下。

nyc

nyc用于统计我们的单测代码测试覆盖率,使用起来也很简单:在测试脚本前加上nyc即可。

代码语言:javascript复制
{
  "scripts": {
    "test": "mocha",
    "coverage": "nyc npm run test"
  }
}

babel

使用babel可以让我们使用es6的语法编写单测代码。

首先我们需要安装babel包:

npm i -D @babel/cli @babel/core @babel/preset-env @babel/register

然后添加babel配置文件:

代码语言:javascript复制
// .babelrc.js
{
    "presets": ["@babel/preset-env"]
}

然后给mocha命令添加参数,指定使用babel进行编译:

mocha --require @babel/register

如果觉得命令行参数太多太长,mocha允许我们使用配置文件的方式来进行传参:

代码语言:javascript复制
module.exports = {
    require: ["@babel/polyfill", "@babel/register"], // 运行单测代码时需要使用babel解析
    recursive: true, // 深度遍历指定目录
    spec: 'test/**/*.test.js', // 运行test目录下的所有单测代码
}

编写单测

完成mocha的插件配置和环境搭建后,终于到了写代码环节了。其实个人觉得单元测试中最重要的环节应该是梳理业务流程,如果能把业务流程梳理为清晰的流程图,写起单测来也会事半功倍。

在编写代码前我们需要来了解下mocha的运行规则,下面是一份测试加法运算函数的单测代码:

代码语言:javascript复制
import getResult from 'add.js'
import { assert } from 'chai'

describe('测试getResult函数', function() {
  it ('正常入参', function() {
    const res = getResult(123);
    assert(res != null, '函数正常入参执行错误')
  })

  it ('非法入参', function() {
    const res = getResult();
    assert(res == null, '函数非法入参执行错误')
  })
})

每一份单元测试脚本都应该至少包含一个describe模块,describe定义了一组逻辑相关的测试组,第一个入参是测试组的名称,第二个入参是交给mocha框架执行的函数。函数内会包含由it定义的测试用例,用来测试该测试组的不同分支。

完整的单测至少应该包含正反方向测试,即测试函数的正常逻辑和异常逻辑。可以看到上述代码定义了一个describe组来测试getResult函数的功能,里面有两个测试用例分别测试了入参正常和非法入参的情况。

而测试用例中如何来判断函数是否正常执行呢?这时候就要用到断言了。

chai断言库

mocha可以搭配你喜欢的任何断言库,经常使用到的有chai断言库。

chai提供了多种风格语法去帮助我们判断函数的执行结果。上述例子中用的是assert语法,它是基于node的assert模块进行扩展。

简单来说,断言库就是帮助我们去判断某些变量是否符合我们的要求,并且在不符合时做出错误提示。

举个:chestnut::

代码语言:javascript复制
assert(res != null, '函数正常入参执行错误')

就是判断res是否不等于null。当第一个入参的表达式结果为false时,表示不符合预期,这是测试用例不通过,会打印出第二个入参的提示语。

异步逻辑

上述的单测例子里,被测试的函数只有同步逻辑,而在js中,异步逻辑无处不在。那么对于异步逻辑需要怎么测试呢?

mocha提供了两种方法来解决这个问题:

promise

我们可以返回一个promise给mocha框架,等到promise的状态改变时再执行断言:

代码语言:javascript复制
it('测试异步函数', function() {
  return fetch('url')
  .then((res) => {
    assert.isObject(res.json(), 'res should be an object');
  })
})

显示返回done

it方法的第二个入参是一个执行函数,我们可以给这个函数传入一个done方法,等到异步返回后再去显示地调用done方法,告诉mocha该测试用例执行完毕。

代码语言:javascript复制
it('测试异步函数', function(done) {
  fetch('url')
  .then((res) => {
    assert.isObject(res.json(), 'res should be an object');
    done();
  })
})

另外需要注意的是,mocha默认每个测试用例的超时时间为2000毫秒,如果超时就会报错。当我们的异步逻辑耗时较长时,需要手动地调整这个超时时间。

我们可以在mocha启动时传入timeout参数,或者在测试用例中显示声明该测试用例的超时时间。

代码语言:javascript复制
it('take less than 5000ms', function(){
  this.timeout(5000);
})

难以模拟的逻辑

在测试服务接口时,总会遇到一些难以模拟或者说不能随便执行的逻辑。

例如当我们需要对一个删除数据的接口进行测试时,我们不能真的去执行数据库删除操作来判断函数是否正常执行。这时候就需要引入sinon来帮助我们替换掉这些难以模拟的逻辑。

sinon库提供了三种功能:spies、stub和mock。

spies

spies功能顾名思义就是间谍函数,它能帮助我们去收集被监听函数的有关调用信息。spies作为sinon最简单的功能,它不会对被监听函数的执行过程造成任何影响,stub和mock功能都是基于spies实现的。

这里再举个:chestnut::

代码语言:javascript复制
import util from '../util';

it('call example function once', function(){
  const spyGetTime = sinon.spy(util, 'getTime');
  exampleFunction();
  assert(util.getTime.calledOnce)
  // 释放
  spyGetTime.restore()
})

上述例子中,exampleFunction 内部调用了 util 的 getTime 方法。而在测试用例开头我们使用 spy 方法监听了 util 的 getTime 方法。调用了 exampleFunction 后,我们可以通过检查 getTime 方法的 calledOnce 属性来判断 getTime 有没有被成功调用。

除了calledOnce 属性之外,spy还在监听函数上挂载了很多监控属性,读者可以自行打印出来看看。

stub

stub可以看做是spy的加强版,它不仅支持spy的各种收集行为,还能控制函数的行为。我们常常使用它来替换掉测试困难的代码部分,例如数据库操作、网络请求等。

代码语言:javascript复制
it('测试格式化函数', function(){
  const stub = sinon.stub(db, 'query').returns({data: []})
  const res = db.query()
  const formatData = format(res)
  assert.isObject(formatData, '格式化函数返回错误')
  stub.restore()
})

上述代码中使用sinon.stub替换了db的query方法,并且控制了其返回值。被替换函数的原有逻辑不会被执行,这样我们就可以通过替换的方式跳过db操作,直接测试后续的format函数。

我们也可以让替换函数主动抛出错误,来测试调用它的函数是否可以正确处理异常:

代码语言:javascript复制
it('测试db操作失败', async function(){
  const stub = sinon.stub(db, 'query').throws(new Error('db error'))
  // queryFunction内部调用了db.query
  const res = await queryFunction();
  assert.isNotNull(res.error, 'res error should not be null')
  stub.restore()
})

生命周期

细心的读者应该发现了我们每次在单元测试开始和结束前都需要做一些准备工作,要么是stub函数,要么是准备mock数据。mocha提供了四个生命周期钩子,我们可以把一些可以复用的准备工作放到钩子中去:

代码语言:javascript复制
describe('test hook', function(){
  before(function() {
    // 在本组测试用例开始前会执行
  })
  after(function() {
    // 在本组测试用例结束后会执行
  })
  beforeEach(function() {
    // 在本组每个测试用例开始前会执行
  })
  afterEach(function() {
    // 在本组每个测试用例结束后会执行
  })
})

superTest

回到我们的文章主题上来,如果我们想要从请求开始来测试node服务接口返回的数据是否正常,也就是说进行一个整体性测试,那么 superTest 就是一个非常好的选择。

superTest可以帮助我们去请求本地 koa 或者 express这类web框架所编写的路由接口,而且对接口返回的状态码、数据等进行断言校验。

它本身不依赖任何测试框架,所以我们可以直接把它丢到mocha的测试用例中执行:

代码语言:javascript复制
const request = require('supertest');
const express = require('express');

const app = express();

app.get('/user', function(req, res) {
  res.status(200).json({ name: 'john' });
});

describe('GET /user', function() {
  it('responds with json', function() {
    return request(app)
      .get('/users')
      .set('Accept', 'application/json')
      .expect('Content-Type', /json/)
      .expect(200)
      .then(response => {
          assert(response.body.name == 'john', '返回数据错误')
      })
  });
});

总结

mocha本身是一个比较简单的测试框架,在此基础上,我们使用一些npm包来加强我们的测试过程:

  • nyc: 提供全面的测试覆盖率
  • chai: 多种风格的断言判断
  • sinon: 用于模拟或者替换难以测试的代码
  • superTest:提供集成测试接口能力

0 人点赞