博客背景:单元测试作为今年的全组通用任务,要求在所有项目中实施,每个人都需要会写单元测试。所以我在上周进行了一下单元测试的调研,这次调研的方向是主要使用 Mocha 基于 Karma 进行包括 UI 层的单元测试。

下面我主要描述一下搭建这套单元测试环境和开发的所用技术,和具体的 demo。

使用的工具介绍

  1. 使用 JavaScript 测试执行过程管理工具 Karma
    Karma是一个基于 Node.js 的 JavaScript 测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器。这个测试工具的一个强大特性就是,它可以监控(Watch)文件的变化,然后自行执行,通过 console.log 显示测试结果。

  2. 单元测试框架 Mocha
    Mocha 是 JavaScript 的一种单元测试框架,既可以在浏览器环境下运行,也可以在 Node.js 环境下运行。

  3. 断言库 Chai
    Chai 是一个针对 Node.js 和浏览器的行为驱动测试和测试驱动测试的断言库,可与任何 JavaScript 测试框架集成。

  4. 测试辅助工具 Sinon
    Sinon 是一个独立的 JavaScript 测试 spy, stub, mock库,没有依赖任何单元测试框架工程。

Karma 的部分 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// karma.conf.js
module.exports = function(config) {
config.set({

// Type: String。默认为""。将用于解析files和exclude中定义的所有相对路径的根路径位置。如果basePath的配置是一个相对路径,那么它将被解析到__dirname的配置文件中。
basePath: '',

// Type: Array。默认为[]。你要使用的测试框架列表。通常情况下,你会设置该值为['jasmine'], ['mocha'] 或 ['qunit']…
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
// frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
frameworks: ['mocha', 'sinon-chai', 'source-map-support'],

// Type: Array。默认为[]。在浏览器中加载的文件/模式列表。
files: [
// {pattern: 'test/**/*.js', included: false}
'src/**/*.test.js'
],

// Type: Array。默认为[]。从加载文件中排除的文件/模式的列表
exclude: [
],

// Type: Object。默认为{'**/*.coffee': 'coffee'}。要是用的预处理器的映射。预处理器可以通过插件加载。
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'src/**/*.test.js': ['webpack', 'sourcemap']
},

// Type: Array。默认为 ['progress']。
// 可能的值:
// · dots
// · progress。
// 使用的报告者(reporter)列表。
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['spec', 'coverage'],

// Type: Number。默认值为9876。设置将被web服务器监听的端口。
port: 9876,

// Type: Boolean。默认为true。启用或禁用输出(报告和日志)的颜色
colors: true,

// Type: Constant。默认为config.LOG_INFO。
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_ERROR,

// Type:Boolean。默认为true。启用或禁用监视文件,当这些文件被改变时,执行测试。
autoWatch: true,

// 该值是要启动和捕获的浏览器列表。当Karma启动时,它也会启动放置在这个设置中的每个浏览器。一旦Karma关闭,它也会关闭这些浏览器。您可以通过打开浏览器并访问Karma Web服务器正在侦听的URL来手动捕获任何浏览器(默认情况下为http://localhost:9876/)。
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
// browsers: ['PhantomJS'],
browsers: ['Chrome'],

// Type: Boolean。默认为false。持续集成模式。
// 如果该值为true,karma将会启动和捕获配置的浏览器,运行测试然后退出,退出使用的代码0或1取决于测试是成功还是失败。
singleRun: false,

// Type: Number。默认为"Infinity"。karma并行启动多少个浏览器。使用该配置,你可以指定在同一时间点上,一次运行多少个浏览器。
concurrency: Infinity,

// coverage 配置项
coverageReporter: {
reporters:[
{type: 'html', dir: 'coverage/'},
{type: 'text-summary'}
]
}
}
}

Mocha 的部分 API

1
2
3
4
5
describe('标题', function() {
it('断言内容', function() {
// 断言部分
});
});

Chai 的部分 API

Chai 支持 BDD 风格的 expect/should API 和 TDD 风格的 Assert API。

expect 和 should是 BDD 风格的,二者使用相同的链式语言来组织断言,但不同在于他们初始化断言的方式:expect 使用构造函数来创建断言对象实例,而 should 通过为 Object.prototype 新增方法来实现断言(所以 should 不支持 IE);expect 直接指向chai.expect,而 should 则是 chai.should()。

语言链

下面的接口是单纯作为语言链提供以期提高断言的可读性。除非被插件改写否则它们一般不提供测试功能。

to be been is that which and has have with at of same

.not

对之后的断言取反

1
2
3
4
expect(foo).to.not.equal('bar')
expect(goodFn).to.not.throw(Error)
expect({ foo: 'baz'}).to.have.property('foo')
.and.not.equal('bar')

.deep

设置 deep 标记,然后使用 equal 和 property 断言。该标记可以让其后的断言不是比较对象本身,而是递归比较对象的键值对。

1
2
3
expect(foo).to.deep.equal({ bar: 'baz'})
expect({ foo: { bar: { baz: 'quux'}}})
.to.have.deep.property('foo.bar.baz', 'quux')

.a(type) / .an(type)

type:String,被测试的值的类型
a 和 an 断言即可作为语言链又可作为断言使用

1
2
3
// 类型断言
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');

.include(value) / contains(value)

value:Object | String | Number

include() 和 contains() 即可作为属性类断言前缀语言链又可作为作为判断数组、字符串是否包含某值的断言使用。当作为语言链使用时,常用于 key() 断言之前

1
2
3
expect([1, 2, 3]).to.include(2)
expect('foobar').to.include('bar')
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo')

.ok

断言目标为真值。

1
2
3
4
expect('everything').to.be.ok
expect(1).to.be.ok
expect(false).to.not.be.ok
expect(null).to.not.be.ok

.true

断言目标为 true。注意,这里与 ok 的区别是不进行类型转换,只能为 true 才能通过断言

1
2
expect(true).to.be.true
expect(1)to.not.be.true

.false

断言目标为 false

1
2
expect(false).to.be.false
expect(0).to.not.be.false

.NaN

断言目标为非数字 NaN

1
2
expect('foo').to.be.null
expect(4)to.not.be.null

.exist

断言目标存在,即非 null 也非 undefined

1
2
3
4
5
6
7
var foo = 'hi',
bar = null,
baz

expect(foo).to.exist
expect(bar).to.not.exist
expect(baz).to.not.exist

.empty

断言目标的长度为 0。对于数组和字符串,它检查 length 属性,对于对象,它检查可枚举属性的数量

1
2
3
expect([]).to.be.empty
expect('').to.be.empty
expect({}).to.be.empty

Sinon API 介绍

辅助工具库 Sinon 主要有三个Api:spy, stub, mock

spy

翻译过来的意思是 “监视”。

sinon.js 中 spy 主要用来监视函数的调用情况,sinon 对待监视的函数进行 wrap 包装,因此可以通过它清楚的知道,该函数被调用过几次,传入什么参数返回什么结果,甚至是抛出的异常情况。

1
2
var spy = sinon.spy(orginObj, 'launch');
spy.restore();

当 spy 使用完成后,切记把它恢复成原始函数,就像上边例子中最后一步那样。如果不这样做,你的测试可能会出现不可预知的结果。

stub

使用 stub 来嵌入或者直接替换掉一些代码,来达到隔离的目的。stub 是代码的一部分。在运行时用 stub 替换真正代码,忽略调用代码的原有实现。目的是用一个简单一点的行为替换一个复杂的行为,从而独立地测试代码的某一部分。它拥有 spy 提供的所有功能,区别在于它会完全替换掉目标函数,而不只是记录函数的调用信息。换句话说,当使用 spy 时,原函数还会继续执行,但使用 stub 时就不会。

Mocks

Mocks 是使用 stub 的另一种途径。如果你曾经听过“mock 对象”这种说法,这其实是一码事 —— Sinon 的 mock 可以用来替换整个对象以改变其行为,就像函数 stub 一样。

单元测试 Demo

这里的一些 Demo,结合了公司内部的代码进行了实际单元测试的书写,因为涉及公司业务代码,暂不公开。请前往公司 gitlab 查看相关 Demo。

  1. 正常单元测试,git地址:https://git.ms.netease.com/changxiao/unitTest

  2. 基于 Vue 开发的组件进行 UI 层测试,主要测试 Dom 的改变,事件的触发。git 地址:https://git.ms.netease.com/guessing/fe/tree/f_unit

参考

https://mochajs.org/
https://cn.vuejs.org/v2/guide/unit-testing.html
http://blog.csdn.net/maomaolaoshi/article/details/78542837
https://www.cnblogs.com/wyqlxy/p/7131079.html
http://blog.csdn.net/hustzw07/article/details/74178051
http://www.zcfy.cc/article/sinon-tutorial-javascript-testing-with-mocks-spies-stubs-422.html