前端单元测试技术方案总结

本文作者: 江水

本文主要介绍前端单元测试的一些技术方案。

单元测试的技术方案很多,不同工具之间有互相协同,也存在功能重合,给我们搭配测试方案带来不小的困难,而且随着 ES6, TypeScript 的出现,单元测试又增加了很多其他步骤,完整配置起来往往需要很大的时间成本。我希望通过对这些工具的各自作用的掌握,了解完整的前端测试技术方案。前端单元测试的领域也很多,这里主要讲对于前端组件如何进行单元测试,最后会主要介绍下对于 React 组件的一些测试方法总结。

通用测试

单元测试最核心的部分就是做断言,比如传统语言中的 assert 函数,如果当前程序的某种状态符合 assert 的期望此程序才能正常执行,否则直接退出应用。所以我们可以直接用 Node 中自带的 assert 模块做断言。

用最简单的例子做个验证

function multiple(a, b) {
    let result = 0;
    for (let i = 0; i < b; ++i)
        result += a;
    return result;
}

const assert = require('assert');
assert.equal(multiple(1, 2), 3));

这种例子能够满足基础场景的使用,也可以作为一种单元测试的方法。

nodejs 自带的 assert 模块提供了下面一些断言方法,只能满足一些简单场景的需要。

assert.deepEqual(actual, expected[, message])
assert.deepStrictEqual(actual, expected[, message])
assert.doesNotMatch(string, regexp[, message])
assert.doesNotReject(asyncFn[, error][, message])
assert.doesNotThrow(fn[, error][, message])
assert.equal(actual, expected[, message])
assert.fail([message])
assert.ifError(value)
assert.match(string, regexp[, message])
assert.notDeepEqual(actual, expected[, message])
assert.notDeepStrictEqual(actual, expected[, message])
assert.notEqual(actual, expected[, message])
assert.notStrictEqual(actual, expected[, message])
assert.ok(value[, message])
assert.rejects(asyncFn[, error][, message])
assert.strictEqual(actual, expected[, message])
assert.throws(fn[, error][, message])

自带的 assert 不是专门给单元测试使用, 提供的错误信息文档性不好,上面的 demo 最终执行下来会产生下面的报告:

$ node index.js
assert.js:84
  throw new AssertionError(obj);
  ^

AssertionError [ERR_ASSERTION]: 2 == 3
    at Object. (/home/quanwei/git/index.js:4:8)
    at Module._compile (internal/modules/cjs/loader.js:778:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)

由于自带的模块依赖 Node 自身的版本,没办法自由升级,所以使用内置的包灵活性有时候不太够,另外我们很多断言函数也需要在浏览器端执行,所以我们需要同时支持浏览器和 Node 端的断言库。同时观察上面的输出可以发现,这个报告更像是程序的错误报告,而不是一个单元测试报告。而我们在做单元测时往往需要断言库能够提供良好的测试报告,这样才能一目了然地看到有哪些断言通过没通过,所以使用专业的单元测试断言库还是很有必要。

chai

chai 是目前很流行的断言库,相比于同类产品比较突出。chai 提供了 TDD (Test-driven development)和 BDD (Behavior-driven development) 两种风格的断言函数,这里不会过多介绍两种风格的优缺,本文主要以 BDD 风格做演示。

TDD 风格的 chai

var assert = require('chai').assert
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

assert.typeOf(foo, 'string'); // without optional message
assert.typeOf(foo, 'number', 'foo is a number'); // with optional message
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(foo, 3, 'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');

chaiNode 自带的 assert 增加了一个断言说明参数,可以通过这个参数提高测试报告的可读性

$ node chai-assert.js

/home/quanwei/git/learn-tdd-bdd/node_modules/chai/lib/chai/assertion.js:141
      throw new AssertionError(msg, {
      ^
AssertionError: foo is a number: expected 'bar' to be a number
    at Object. (/home/quanwei/git/learn-tdd-bdd/chai-assert.js:6:8)
    at Module._compile (internal/modules/cjs/loader.js:778:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)

BDD 风格的 chai

chaiBDD 风格使用 expect 函数作为语义的起始,也是目前几乎所有 BDD 工具库都遵循的风格。

chaiexpect 断言风格如下

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);

BDD 的思想就是写单元测试就像写产品需求,而不关心内部逻辑,每一个用例阅读起来就像一篇文档。例如下面的用例:

  1. foo 是一个字符串 ->expect(foo).to.be.a('string')
  2. foo 字符串里包含 'bar' ->expect(foo).to.include('bar')
  3. foo 字符串里不包含 'biz' -> expect(foo).to.not.include('biz')

可以看到这种风格的测试用例可读性更强。

其他的断言库还有 expect.js should.js better-assert , unexpected.js 这些断言库都只提供纯粹的断言函数,可以根据喜好选择不同的库使用。

有了断言库之后我们还需要使用测试框架将我们的断言更好地组织起来。

mocha 和 Jasmine

mocha 是一个经典的测试框架(Test Framework),测试框架提供了一个单元测试的骨架,可以将不同子功能分成多个文件,也可以对一个子模块的不同子功能再进行不同的功能测试,从而生成一份结构型的测试报告。例如 mocha 就提供了describeit 描述用例结构,提供了 before, after, beforeEach, afterEach 生命周期函数,提供了 describe.only ,describe.skip , it.only, it.skip 用以执行指定部分测试集。

const { expect } = require('chai');
const { multiple } = require('./index');

describe('Multiple', () => {
    it ('should be a function', () => {
        expect(multiple).to.be.a('function');
    })

    it ('expect 2 * 3 = 6', () => {
        expect(multiple(2, 3)).to.be.equal(6);
    })
})

测试框架不依赖底层的断言库,哪怕使用原生的 assert 模块也可以进行。给每一个文件都要手动引入 chai 比较麻烦 ,这时候可以给 mocha 配置全局脚本,在项目根目录 .mocharc.js 文件中加载断言库, 这样每个文件就可以直接使用 expect 函数了。

// .mocharc.js
global.expect = require('chai').expect;

使用 mocha 可以将我们的单元测试输出成一份良好的测试报告 mocha *.test.js

当出现错误时输出如下

因为运行在不同环境中需要的包格式不同,所以需要我们针对不同环境做不同的包格式转换,为了了解在不同端跑单元测试需要做哪些事情,可以先来了解一下常见的包格式。

目前我们主流有三种模块格式,分别是 AMD, CommonJS, ES Module

AMD

AMDRequireJS 推广过程中流行的一个比较老的规范,目前无论浏览器还是 Node 都没有默认支持。AMD 的标准定义了 definerequire函数,define用来定义模块及其依赖关系,require 用以加载模块。例如



    
        
        Document
+        
+