function sayHello(name){
return name ? 'hi '+name : 'no one.'
}
测试文件:test/index.test.js
describe('Index',function () {
it('should say hi give a name',function () {
expect(sayHello('LCT')).to.equal('hi LCT');
});
it('should say no one if not give a name',function () {
expect(sayHello()).to.equal('no one.')
})
})
describe('User', function() {
describe('#save()', function() {
it('should save without error', function(done) {
var user = new User('Luna');
user.save(function(err) {
if (err) done(err);
else done();
});
});
});
});
describe('hooks', function() {
before(function() {
// runs before all tests in this block
});
after(function() {
// runs after all tests in this block
});
beforeEach(function() {
// runs before each test in this block
});
afterEach(function() {
// runs after each test in this block
});
// test cases
});
beforeEach(function() {
console.log('before every test in every file');
});
这个根Hooks会在其他文件中describe执行之前被执行。
EXCLUSIVE 和 INCLUSIVE
Mocha使用only()来选择执行一个特定的套件或者测试用例。
describe('Array', function() {
describe.only('#indexOf()', function() {
it.only('should return -1 unless present', function() {
// this test will be run
});
it.only('should return the index when present', function() {
// this test will also be run
});
it('should return -1 if called with a non-Array context', function() {
// this test will not be run
});
});
describe.only('#concat()', function () {
it('should return a new Array', function () {
// this test will also be run
});
});
describe('#slice()', function () {
it('should return a new Array', function () {
// this test will not be run
});
});
});
describe('Array', function() {
describe('#indexOf()', function() {
it.skip('should return -1 unless present', function() {
// this test will not be run
});
it('should return the index when present', function() {
// this test will be run
});
});
});
skip的功能就相当于把该段代码注释掉,不让它执行,也不会在结果中显示。
pending
it()不传入回调函数,则该用例就处于pending(待定,待完成)状态。
describe('Array', function() {
describe('#indexOf()', function() {
// pending test below
it('should return -1 when the value is not present');
});
});
function sayHello(name){
return name ? 'hi '+name : 'no one.'
}
module.exports = sayHello;
新建test/index.test.js文件:
var sayHello = require('../src/index.js');
var expect = require('chai').expect;
describe('Index',function () {
it('should say hi give a name',function () {
expect(sayHello('LCT')).to.equal('hi LCT');
});
it('should say no one if not give a name',function () {
expect(sayHello()).to.equal('no one.')
})
})
function sayHello(name){
return name ? 'hi '+name : 'no one.'
}
新建一个test/index.test.js文件:
var expect = chai.expect;
describe('Index',function () {
it('should say hi give a name',function () {
expect(sayHello('LCT')).to.equal('hi LCT');
});
it('should say no one if not give a name',function () {
expect(sayHello()).to.equal('no one.')
})
})
function sayHello(name){
return name ? 'hi '+name : 'no one.'
}
module.exports = sayHello;
改写test/index.js,使用commonjs规范引入接口:
var sayHello = require('../src/index');
var expect = chai.expect;
describe('Index',function () {
it('should say hi give a name',function () {
expect(sayHello('LCT')).to.equal('hi LCT');
});
it('should say no one if not give a name',function () {
expect(sayHello()).to.equal('no one.')
})
})
function sayHello(name){
return name ? 'hi '+name : 'no one.'
}
export default sayHello;
改写test/index.js,使用es6规范:
import sayHello from '../src/index';
var expect = chai.expect;
describe('Index',function () {
it('should say hi give a name',function () {
expect(sayHello('LCT')).to.equal('hi LCT');
});
it('should say no one if not give a name',function () {
expect(sayHello()).to.equal('no one.')
})
})
前端(单元)测试入门
本文将从前端测试(主要是单元测试)的基本概念,到常用工具的介绍,结合实际的示例讲解前端测试。然后再到实际项目的综合测试,最终将介绍目前较为流行的react + redux + webpack + e6架构的测试。
前端测试可以使用的工具非常多,,下面列举一些常用工具和参考教程。
下面我们先从基本概念说起。
前端测试的基础知识
单元测试是一种软件测试方法,是最小粒度的测试,需要由开发人员来完成。单元测试通常是指对一个module单元或者一个函数进行测试,通过控制数据、使用和操作过程来检验该单元是否可以使用或者达到我们预期的功能。通俗点讲,就是检验参数和返回结果。多个单元应该可以独立的平行的进行测试。
前端开发中,我们使用的开发语言多少JavaScript,因此我们也更多的关注的是JavaScript的测试。关于JavaScript的测试框架有很多,jasmine,mocha等等。另外还有一个js单元测试辅助库Sinon,它能帮助我们更好的完成Function级别的测试。针对最近比较火的react,除了react官方提供的测试方法外,还有一个更加优秀的测试框架enzyme 。
前端开发的程序一般会运行在不同浏览器或者平台下,而我们的js程序在不同浏览器下的执行结果可能也会不同,karma可以帮助我们自动的完成在不同浏览器下的测试过程。karma是一个测试执行过程管理工具(test runner),它除了有跨平台的在真实浏览器测试的功能外,还有其他许多优点,比如远程控制,测试速度快,生成测试报告等。
开发好了一个web应用程序,通常我们需要模拟真实用户来使用系统,webdriverio可以帮助我们模拟用户在浏览器上执行操作,它是基于浏览器的e2e(end to end 端对端)的测试,当然也可以用来检验我们的UI界面,并且它还提供了移动端平台的测试。
在接触测试的过程中,会涉及许多概念,单元测试,集成测试,自动化测试等等概念。如果有对测试的概念不清楚的童鞋,请查看http://www.cnblogs.com/TankXiao/archive/2012/02/20/2347016.html https://segmentfault.com/a/1190000000317146这两篇文章。
mocha
mocha是一个JavaScript测试框架,它可以运行在nodejs和浏览器环境中。先来看看mocha是如果对JavaScript代码进行测试的。
待测试文件:src/index.js
测试文件:test/index.test.js
runner文件:index.html
浏览器运行,查看结果:
上面代码验证了sayHello方法是否返回了正确的值,由结果可以看到两条测试都通过了。本例仅仅只是展示了mocha能干什么,其中的语法容后介绍。
mocha安装
mocha项目地址:https://github.com/mochajs/mocha,进入release,然后选择最新的zip包进行下载。在加载测试文件之前,需要使用mocha.setup('bdd')来指定mocha使用BDD(行为驱动测试)接口,最后使用mocha.run()来开启测试。
nodejs环境安装:
or
执行测试文件使用mocha命令:
环境有了,现在我们来学习怎么写测试代码。
mocha基本概念
describe
describe是一个全局函数,describe定义一组相关的测试,被称为“测试套件”(test suite)。它需要接受两个参数,第一个参数是测试套件的名字,可以是任意的字符串;第二个参数接受一个函数,是测试套件的主体内容,该函数会在套件被加载时执行。
可以定义多个describe,describe还可以被嵌套,各个describe会依次被执行。
it
it对应的是一个测试用例(test case)。它跟describe一样也接受两个参数,第一个参数是用例的名称;第二个参数是用例要执行的内容。
一个套件可以包含多个用例,各个用例依次被执行。用例中应该对测试结果进行断言(验证与预期结果是否一致)。
expect
上面示例代码中每个it中都包含了一个expect语句,该语句就是一个断言语句。expect所做的工作就是判断源码的实际执行结果是否与我们期望的结果一致,如果一致则该断言通过,如果不一致则不通过。
it中可以包含多句断言,只有所有的断言都通过时,该测试用例才通过。
断言库有很多,mocha并不限制使用哪一种,上面的示例代码用的是chai,后面再详细介绍chai断言库。可使用的断言库shoud.js、better-assert、expect.js、unexpected、chai。
mocha高级用法
异步测试
通过给it()函数传递一个回调函数(通常叫做done,这也是约定),然后在测试代码完成时调用done函数,mocha便能知道该函数需要等待完成。
这是官方的一个异步示例代码。也可以直接在it()返回一个Promise。
mocha则知道需要等待该Promise状态改变,然后再执行断言。
需要注意的是返回了Promise则不应该再使用done函数,在3.0.0版本以后会直接报错,之前版本会忽略done。
如果使用了异步测试代码,则mocha不会等待it()中回调函数执行完成,而是自动的继续执行下一个测试用例。
Hooks
mocha提供了 before()、 after()、 beforeEach()、和 afterEach() 4个钩子。可以使用它们来对测试进行一个预处理或者清除工作。
before会在所有测试用例之前被调用,只执行一次。after会在所有测试用例之后调用, 也只被调用一次。beforeEach会在每个测试用例被调用之前都执行一次,afterEach同理。
Hooks可以用在嵌套的describe中,当执行到嵌套的describe时,执行hooks。
Hooks可以传递一个字符串作为第一个参数,用来对Hooks进行描述,这个字符串会被打印出来,方便跟踪Hooks的执行情况。如果没有传入字符串,则函数名作为默认的标示,匿名函数则忽略。
Hooks也可以进行异步操作。
这是官方使用的异步的Hooks,它的行为就跟普通的it()一样,done()被执行才标志该Hooks结束,而在这之前不会等待,继续向下执行嵌套的describe。
根套件
Mocha有一个隐藏的describe块,被称为根套件。根套件一般会在一个文件的开始,在所有的显式的describe和测试用例之前执行,并且不论这个文件在什么地方,它总会被先执行。
基于此原理我们可以在describe之前写Hooks,叫做Root Hooks,一般会直接新建一个文件,然后书写一个Hooks。
这个根Hooks会在其他文件中describe执行之前被执行。
EXCLUSIVE 和 INCLUSIVE
Mocha使用only()来选择执行一个特定的套件或者测试用例。
only()可以被使用在describe或者it中,平行结构中可以定义多个only(),被only标示的会被执行,在平行结构中只要有only(),则只有only会被执行,其他的会忽略。
与only()相反的是skip(),被skip()标示的会被忽略,而其他的会被执行。skip也可用在describe和it中。
skip的功能就相当于把该段代码注释掉,不让它执行,也不会在结果中显示。
pending
it()不传入回调函数,则该用例就处于pending(待定,待完成)状态。
pending发生的一般情况是项目经理写好了测试用例结构,但是用例具体实现需要组员来补充完整时使用pending。pending会被显示在结果中。
mocha的更多使用方法,超时,动态生成测试用例,Diff等请参考官方地址。
mocha在node环境下的例子
新建一个项目,并初始化package.json文件:
安装mocha依赖和chai断言库:
全局安装mocha命令行工具:
新建src/index.js文件:
新建test/index.test.js文件:
执行测试:
结果如下:
mocha在node环境中使用时,可以指定一个入口文件,然后其他的文件通过commonjs规范来进行引入。
chai断言库
安装
NodeJS安装
浏览器使用
本文只介绍chai BDD模式下的断言。chai BDD模式的断言风格可以是expect和shoud。下面先介绍expect的用法,然后再介绍chai扩展的API。
expect.js
expect.js项目地址:https://github.com/Automattic/expect.js
下面列举的API均使用函数调用形式,更多的示例请参考官方网站。
expect([]).to.be.an('object'); // works, since it uses
typeof
expect([]).to.be.an(Array);
expect(program.version).to.match(/[0-9]+.[0-9]+.[0-9]+/);
expect(window).to.have.property('expect'); expect(window).to.have.property('expect', expect);
expect({ a: 'b' }).to.have.key('a');
expect({ a: 'b', c: 'd' }).to.only.have.keys('a', 'c');
以上列举了一些常用的api,更多api和示例请参考官网。
chai扩展和重写了expect
chai可以使用expect风格的断言,并且在expect的基础上进行了重写,使其变得更加可读和专用。
chai的官网对其api进行了详细的描述,这里只是做一个简单的翻译和expect的对比。
expect扩展了链式调用,增加了许多链式调用的连接词。这些连接词并不具有测试功能,除非通过插件重写,不然它仅仅代表着语义所表示的连接含义,相当于谓语。断言之间应该至少包含一个连接词to。
这里介绍几个常用的api,其余的api请参照官网。
.deep 执行对象深比较,一般用在equal和property之前。
使用.deep.property断言属性时,属性中包含.或者[]使用两个斜杠进行转义。
karma
karma简介
karma是一个测试管理工具,一个test runner。这个工具可以为我们配置所有主流浏览器的测试,也可以集成到CI(持续集成)测试。mocha、jasmine等工具为我们提供了测试的方法,而karma让我们把测试的用例代码运行在不同的平台,而且变得自动化。
karma基于NodeJS,因此使用karma需要先安装nodejs。
karma的安装
执行如下命令安装:
现在karma已经安装到本地开发环境,当然也可以进行全局安装。除了全局安装karma以外,我们可以仅karma的命令行工具到全局环境,这样我们一样可以使用karma命令。
现在我们可以启动karma了。
命令行的显示如下:
表示我们已经在9876端口(默认端口)启动了一个服务,我们用浏览器打开这个URL。
可以看到karma已经启动成功了。然后一般情况下点击DEBUG按钮就可以进行运行测试了,但是现在并没有反应,那是因为我们还未配置任何的测试。
karma的配置文件
karma通过config文件来配置测试环境。通过执行karma init命令会自动生成一个karma.conf.js文件。
执行karma init命令会回答一些问题。
这时我们在项目目录下可以看到一个karma.conf.js文件。使用karma start命令时会自动读取这个文件。 同时,在我们的node_modules中,自动添加了mocha、karma-mocha、karma-chrome-launcher三个包。
再次运行karma start,发现现在karma已经可以自动打开chrome浏览器了。
配合mocha进行单元测试
新建一个项目,初始化package.json文件:
安装karma,初始化配置文件并安装mocha依赖:
新建一个src/index.js文件:
新建一个test/index.test.js文件:
上面使用了chai断言库,因此安装chai断言和karma的chai断言插件:
修改config文件,在frameworks属性中加入chai(chai会作为全局变量被引入):
在files属性中加入测试文件(测试文件和待测文件的引入无先后顺序):
启动karma:
命令行显示如下:
浏览器会自动被启动,然后点击DEBUG按钮,会另外打开一个页面,在该页面中查看控制台log信息如下:
由于我们开启了autoWatch功能,因此如果我们修改了文件中内容并保存,在命令行我们马上就能看到执行结果,直接刷新页面也能看到最新的执行结果。如果关闭监控文件功能,则需要重新启动karma才能看到结果。
测试覆盖率
我还可以查看代码的覆盖率。代码覆盖率(coverage)是衡量测试质量的一个标准,用来描述程式中被测代码的比例和和程度。 我们可以使用karma-coverage插件来查看测试的代码覆盖率,安装方式如下:
karma本身使用Istanbul实现覆盖率统计,因此在安装coverage时,已经在coverage的依赖中安装了Istanbul。要使用coverage,需要在配置文件中三个地方进行修改。
在preprocessors属性中添需要进行覆盖率统计的文件,一般我们对待测试的文件进行覆盖率统计才有意义。但是,我们也可以将测试文件也包括进来,看看我们的测试用例是否覆盖完全了。
然后将覆盖率统计输出为测试报告,因此在reporters中添加coverage。
新建coverageReporter节点,设置生成覆盖率报告的格式和路径。
启动karma。然后在TestFiles/coverage文件夹中生成了测试报告,报告是html形式,因此我们可以直接打开index.html来查看覆盖率。同时,还生成了preprocessors节点配置的对应文件(文件夹)的单独的覆盖率统计,文件格式也是html。
结合webpack实现模块化
注意,此时上面的测试代码实际上是运行在浏览器环境中的,因为karma实际上是一个client/server结构的程序,它会自动启动我们配置的浏览器,并且把测试代码运行在浏览器环境中,因此,不能像前面mocha在node环境中那样使用commonjs的模块规范。但是,我们可以通过其他的模块的工具来实现模块化,比如requirejs,只需要在初始化配置文件时,将引入requirejs文件设置为yes即可。
另外,我们可以使用最新的es6的模块规范,也可以使用es6的最新的语法规范。只需要引入babel的相关转码api,再用webpack来管理模块依赖即可。
安装webpack和webpack的karma插件:
改写src/index.js,使用nodejs的commonjs规范导出接口 :
改写test/index.js,使用commonjs规范引入接口:
修改karma配置文件,files属性仅需要添加测试入口即可:
preprocessors属性中指定测试文件需要使用webpack:
启动karma。
可以看到,webpack启动成功了,而且现在使用commonjs模块规范也执行成功了。
使用webpack之后,文件的监控功能其实已经被webpack接管了。所以将autoWatch设置为false后,修改代码也能够立即执行。
可以看到关闭autoWatch之后,修改了代码每次保存webpack都会自动执行,而且刷新页面最新的代码也被执行了,包括对源代码的改动,测试也会立即执行。唯一的区别是没有chrome : Excuted 1 of 2 SUCCESS这样的浏览器信息。这是因为karma-webpack插件默认加载了webpackDevMiddleware,打开了一个Dev server。
然后我们再看测试覆盖率。what?没有源文件覆盖率?测试文件覆盖率不是100%了?
这是因为webpack通过测试入口文件,利用webpack的模块加载机制,将所有的代码都打包到了一个文件中,这时候我们得到的覆盖率就是打包后的文件的覆盖率,因此没有源文件的覆盖率。在打包的过程中,webpack会加入一些其特有的代码,还会引入一些其他库或者函数,所以覆盖率就变得很低。
要解决上面的问题,我们可以使用isparta-loader来对源文件进行预处理。
统计源文件测试覆盖率
对于上面提到的要统计源文件的测试覆盖率可以使用isparta-loader来对源文件进行预处理。
安装步骤如下:
在karma的配置文件中新建一个webpack节点,指定对源文件进行预加载处理:
由于isparta-loader在对源文件进行加载处理时,已经包含了要做覆盖率统计的操作,所以在preprocessors节点中不再需要指定源文件的coverage:
重新执行,再查看覆盖率。我们的覆盖率又回来了!
使用es6规范
添加es6转码工具bable:
改写src/index.js,使用es6规范 :
改写test/index.js,使用es6规范:
在karma配置文件中,添加babel的支持。files和preprocessors不变,在webpack属性节点中添加babel支持:
启动karma。现在已经能通过webpack和babel运行es6代码了。在这里,有些童鞋看的教程说使用export default报错,只能使用module.exports,那是因为使用isparta-loader时,也需要指定其使用babel进行ES6转码,加上上面的isparta节点的配置,程序正常运行。
但是,在实际项目中,我们希望能将webpack的配置信息分离出去,作为一个单独的config文件。
在根目录下新建一个webpack.test.config.js文件,作为webpack的配置文件:
};
注意这里没有entry和output两个节点的配置,打包的输入和输出karma会自己指定。
在karma.conf.js文件中引入webpack的配置文件:
然后webpack的节点指定为引入的webpack配置:
重新启动karma。good,一切功能ok!
更加易用化
单一入口文件
在实际项目中我们会有许多测试文件,不同功能模块的测试会有不同的文件,这时候我们可以使用一个单一的入口文件来加载其他所有的文件,对一个单一的文件配置files或者preprocessors肯定简单的多。
在test文件夹下新建一个test.bundle.js文件:
加载所有的.test.js或者.spec.js文件,如果有多个文件夹,可以多次调用require.context,然后使用keys().forEach()。
有了这个入口文件后,在karma配置文件中只需要引入该文件即可:
这里值得注意的是,test.bundle.js放在项目根目录下执行会报错,所以我放在test目录下。并且,一定要在webpack的preLoaders中剔除test.bundle.js:
现在我们已经有了单一入口文件了。但是,随着项目的增大,文件的增加,webpack打包的速度会越来越慢。其实,webpack打包的速度本身就很慢,所以我们可以关闭autowatch功能。
source map 和report
有了单一入口文件,我们的测试代码也被打包在了一个统一的文件中。但是许多时候,我们需要快速的定位未通过的测试用例具体在哪个文件中的哪一行,这时候source map就派上用场了。
先看未使用source map时,未通过的测试用例的执行情况:
可以看到这里有个测试用例没有通过,但是,我们只知道它在webpack打包后的test.bundle.js中的位置。现在我们使用source map来快速定位到具体文件。
安装source map:
在preprocessors中指定test.bundle.js使用source map:
在webpack中配置devtool属性,可以在karma配置文件中,在引入webpack配置文件时,动态指定:
现在source map已经指定好了,再次运行。
现在已经能快速的定位未通过的测试用例了。但是,目前这种报告信息仍然不够清晰,我们可以使用插件来生成更加直观的report统计。
安装html-reporter:
然后在reporters节点中配置HTML report:
新建一个 htmlReporter节点指定输出位置 :
运行karma之后,在TestFiles/report文件夹中就生成了一个html格式的report。
使用npm命令
将karma和karma-处理都在本地环境安装一遍:
修改package.json文件,自定义test命令:
然后,运行npm命令:
ok!现在我们可以使用npm的短命令了。
关于前端测试的基础部分就介绍到这里,后面我会继续介绍使用enzyme和sinon对react项目进行单元测试,另外也会介绍一下webdriverio的相关用法。
未完待续。。