博客背景:单元测试作为今年的全组通用任务,要求在所有项目中实施,每个人都需要会写单元测试。所以我在上周进行了一下单元测试的调研,这次调研的方向是主要使用 Mocha 基于 Karma 进行包括 UI 层的单元测试。
下面我主要描述一下搭建这套单元测试环境和开发的所用技术,和具体的 demo。
使用的工具介绍
- 使用 JavaScript 测试执行过程管理工具 Karma Karma是一个基于 Node.js 的 JavaScript 测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器。这个测试工具的一个强大特性就是,它可以监控(Watch)文件的变化,然后自行执行,通过 console.log 显示测试结果。
- 单元测试框架 Mocha Mocha 是 JavaScript 的一种单元测试框架,既可以在浏览器环境下运行,也可以在 Node.js 环境下运行。
- 断言库 Chai Chai 是一个针对 Node.js 和浏览器的行为驱动测试和测试驱动测试的断言库,可与任何 JavaScript 测试框架集成。
- 测试辅助工具 Sinon Sinon 是一个独立的 JavaScript 测试 spy, stub, mock库,没有依赖任何单元测试框架工程。
Karma 的部分 API
代码语言:javascript复制// 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
代码语言:javascript复制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
对之后的断言取反
代码语言:javascript复制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 断言。该标记可以让其后的断言不是比较对象本身,而是递归比较对象的键值对。
代码语言:javascript复制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 断言即可作为语言链又可作为断言使用
代码语言:javascript复制// 类型断言
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
.include(value) / contains(value)
value:Object | String | Number
include() 和 contains() 即可作为属性类断言前缀语言链又可作为作为判断数组、字符串是否包含某值的断言使用。当作为语言链使用时,常用于 key() 断言之前
代码语言:javascript复制expect([1, 2, 3]).to.include(2)
expect('foobar').to.include('bar')
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo')
.ok
断言目标为真值。
代码语言:javascript复制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 才能通过断言
代码语言:javascript复制expect(true).to.be.true
expect(1)to.not.be.true
.false
断言目标为 false
代码语言:javascript复制expect(false).to.be.false
expect(0).to.not.be.false
.NaN
断言目标为非数字 NaN
代码语言:javascript复制expect('foo').to.be.null
expect(4)to.not.be.null
.exist
断言目标存在,即非 null 也非 undefined
代码语言:javascript复制var foo = 'hi',
bar = null,
baz
expect(foo).to.exist
expect(bar).to.not.exist
expect(baz).to.not.exist
.empty
断言目标的长度为 0。对于数组和字符串,它检查 length 属性,对于对象,它检查可枚举属性的数量
代码语言:javascript复制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 包装,因此可以通过它清楚的知道,该函数被调用过几次,传入什么参数返回什么结果,甚至是抛出的异常情况。
代码语言:javascript复制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。
- 正常单元测试,git地址:https://git.ms.netease.com/changxiao/unitTest
- 基于 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