自动化测试的意义
以Grunt为基础,构建版本CI。实时监控项目的健康程度,最为极致的情况就是依靠CI保证随时都可以发布版本。因此测试也要满足于可以被Grunt集成,并且可以输出报告。在构建完成后群发给项目全体成员。
测试代码要做分层,也就是测试用例和框架要分离,不能耦合。测试用例的编写人员的技能无需太高,也无需关注框架的架构。
Grunt介绍
Grunt 是一个基于任务的 JavaScript 项目命令行构建工具,运行于 Node.js 平台。Grunt 能够从模板快速创建项目,合并、压缩和校验 CSS & JS 文件,运行单元测试以及启动静态服务器(这些功能都依赖于grunt插件,这些插件都是通过npm安装,并由Github托管)。
Grunt和 Grunt插件是通过 npm 安装并管理的。安装命令:
npm install grunt npm install grunt-cli
每次运行grunt 时,它就利用node提供的require()系统查找本地安装的 Grunt。正是由于这一机制,你可以在项目的任意子目录中运行grunt 。如果找到一份本地安装的 Grunt,CLI就将其加载,并传递Gruntfile中的配置信息,然后执行你所指定的任务。
Mocha介绍
Mocha是node.js中最常用的测试框架,支持多种node的assert libs(should,chai,expect,better-assert等), 多种方式导出结果,也支持直接在浏览器上跑JavaScript代码测试。
Mocha安装
npm install mocha
Mocha及其相关插件简介
TDD&BDD基本定义
TDD (Test Driven Development)关注的是接口是否被正确地实现了,BDD (Behavior Driven Development) 通常以类为单位, 关注一个类是否实现了文档定义的行为。Mocha默认的执行方式是BDD。
Mocha对于TDD&BDD方式的实现
BDD的样例代码如下
//同步方式 var assert = require("assert"); describe(‘Array‘, function () { describe(‘#indexOf()‘, function () { it(‘should return -1 when the value is not present‘, function () { assert.equal(-1, [1, 2, 3].indexOf(5)); assert.equal(-1, [1, 2, 3].indexOf(0)); }) }); describe(‘#join()‘, function () { it(‘should return -1 when the value is not present‘, function () { assert.equal(‘1,2,3‘, [1, 2, 3].join(‘,‘)); }) }) }); //异步方式 fs = require(‘fs‘); describe(‘File‘, function () { describe(‘#readFile()‘, function () { it(‘should read test.ls without error‘, function (done) { fs.readFile(‘test.ls‘, function (err) { if (err) throw err; done(); }); }) }) })
BDD的关键API是describe,并且允许嵌套。 第一层describe指定测试的类,第二层指定测试的成员方法。从第二层起的每一个subscribe都可以看成第一层subscribe所对应的测试方法中的一条语句。这也符合BDD以类为单位的理念。此外BDD提供before,after,beforeEach,afterEach类似于TDD中的setup, teardown这里不再赘述。
TDD代码样例suite(‘Array‘, function () { setup(function () { // ... }); suite(‘#indexOf()‘, function () { test(‘should return -1 when not present‘, function () { assert.equal(-1, [1, 2, 3].indexOf(4)); }); }); });
如何集成Mocha到Grunt
grunt-mocha-test
插件,命令如下
npm install grunt-mocha-test
Gruntfile代码如下,比较简单,这里不做太多说明。
关于mochaTest的选项,参见https://github.com/pghalliday/grunt-mocha-test
mochaTest: { tdd_test: { options: { ui: ‘tdd‘, reporter: ‘spec‘ }, src: [‘<%= pkg.name%>/tdd_api_testcase/prepare_browser.js‘,‘<%= pkg.name%>/tdd_api_testcase/test_*.js‘] }, bdd_test: { options: { reporter: ‘spec‘, output: ‘<%= pkg.name%>/bdd_api_testcase/result.txt‘, require: ‘blanket‘ }, src: [‘<%= pkg.name%>/bdd_api_testcase/*.js‘] }, coverage: { options: { reporter: ‘html-cov‘, // use the quiet flag to suppress the mocha console output quiet: true, // specify a destination file to capture the mocha // output (the quiet option does not suppress this) captureFile: ‘<%= pkg.name%>/coverage.html‘ }, src: [‘<%= pkg.name%>/tdd_api_testcase/*.js‘] }, testReporter: { options: { output: ‘frame_test/result.txt‘ } } },
测试AMD模块以及浏览器环境模拟
在node上测试项目代码,需要解决如下几个问题
A. Node的模块加载器是CMD规范,但是项目的模块加载器是AMD规范。需要定义一个归一化的require。
B. 在requirejs的copnfig中重新指定模块的加载路径。
C. 由于是在本地执行js所以项目中使用绝对路径的地方需要改造。
D. 需要引入jsdom来模拟浏览器环境,使得代码中对于浏览器相关的操作不至于出错
E. 由于jsdom的模拟本身也是异步调用,所以涉及对grunt的源代码调整。
由于每次node加载文件的时候都会覆盖require(参见node源代码module.js),因此需要在测试用例执行前执行如下代码。var assert = require(‘assert‘); var requirejs = require(‘requirejs‘); var c_require = require; requirejs.config({ //Pass the top-level main.js/index.js require //function to requirejs so that node modules //are loaded relative to the top-level JS file. baseUrl: ‘webapp/1.1/web/‘, nodeRequire: require }); require = function (path, callback) { if (arguments.length == 2) { return requirejs.call(global, path, callback); } else { return c_require.call(global, path); } }
测试项目的处理方式为,将以上代码单独做成一个独立的JS文件(case_header.js),并且在GruntFile.js中增加如下代码
//用例头文件 加载 var _compile = module.__proto__._compile; module.__proto__._compile = function (content, filename) { if (filename.indexOf(‘tdd_api_testcase‘) > -1 || filename.indexOf(‘bdd_api_testcase‘) > -1) { content = grunt.file.read(‘case_header.js‘) + content; } _compile.call(this, content, filename); };
对于D,E的解决方式,是让Grunt先以执行测试用例的方式,完成Jsdom的window模拟,对应的TDD模式的测试文件为
suite(‘PrepareBrowser‘, function () { console.log("Prepare browser enviroment"); setup(function (done) { var jsdom = require(‘jsdom‘); jsdom.env({ html: "<DIV/>", documentRoot: ‘<%= pkg.name%>/../../../‘, scripts: [], done: function (errors, window) { global.window = window; for (var p in window) { if (!global[p]) { global[p] = window[p]; } } window.localStorage = {}; window.localStorage.get = function () { return this; } window.localStorage.getItem = function (name) { return this[name]; } window.localStorage.set = function (data) { this = data; } window.localStorage.setItem = function (name, value) { this[name] = value; } done(); } }); }); suite(‘PrepareBrowser‘, function () { test(‘PrepareBrowser‘, function () { if (!window) throw new Error("failed!"); }); }); });
这里要强调的是Jsdom仅是保证在node环境下能访问浏览器的BOM和DOM对象,这个和无头测试是两个概念。Jsdom的基本原理是通过加载一个页面(可以是远程的http地址或者是本地文件抑或是一段HTML)以及js文件来实现对浏览器对象的模拟。在整个MochaTest插件运行的生命周期中,这个HTML的环境是不会变化的。而所谓的无头(headless)测试,指的是有测试html并且测试html可以在浏览器中正确执行只是在测试执行的时候不用启动浏览器。因此通过Jsdom完成无头测试不是一个很好的方案
TDD还是BDD
下面是TDD和BDD代码框架,完成相同的测试
suite(‘TestInherit‘, function () { var core; setup(function (done) { //在Setup中完成模块加载,一个测试套一次 require([‘core/c.core.inherit‘], function (module) { core = module; done(); }); }); suite(‘test_inherit_1‘, function () { test(‘test_inherit_1‘, function () { //测试代码 }); test(‘test_inherit_2‘, function () { //测试代码 }); }); }); |
describe(‘TestInherit‘, function (done) { console.log("Inherit bdd test suite fired!"); it(‘test_inherit_1‘, function (done) { require([‘core/c.core.inherit‘], function (core) { //测试代码 done(); }); }); it(‘test_inherit_2‘, function (done) { require([‘core/c.core.inherit‘], function (core) { //测试代码 done(); }); }); }); |
Jscoverage介绍
jscoverage的原理就是把测试代码每一个逻辑路径都插入一个检查点,当代码运行到那个路径的时候会在计数器里加1,多次运行那段的代码便一直累加上去。这里说一句Jscoverage的开源协议是GPL,使用前需要评估。
准备工作:
l 下载测试覆盖率的工具jscoverage。
在这里http://siliconforks.com/jscoverage/download.html选择一个windows版本下载,放到操作系统的PATH变量中。
安装grunt-jscoverage插件npm install grunt- jscoverage
GruntFile相关配置修改如下
jscoverage: { options: { inputDirectory: ‘frame_test/tdd_api_testcase‘, outputDirectory: ‘frame_test/tdd_api_testcase-cov‘ } }, coverage: { options: { reporter: ‘html-cov‘, // use the quiet flag to suppress the mocha console output quiet: true, // specify a destination file to capture the mocha // output (the quiet option does not suppress this) captureFile: ‘frame_test/coverage.html‘ }, src: [‘<%= pkg.name%>/tdd_api_testcase/*.js‘] } }
用例调试
安装插件node-inspector,相关材料:参见https://github.com/node-inspector/node-inspector
安装完毕后,打开操作系统命令窗口执行node-inspector &
保留此命令窗口不要关闭
用chrome浏览器打开http://127.0.0.1:8080/debug?port=5858
在grunt项目下(注意,一定要在此目录下,由于模块加载配置的是相对路径,所以这个很重要)执行测试用例(红色字体部分是调试参数)
执行API用例的命令行如下
node --debug-brk node_modules\grunt-cli\bin\grunt
刷新地址为http://127.0.0.1:8080/debug?port=5858的浏览器窗口,代码就会载入到浏览器调试器中进行调试。
Grunt File
//测试报告 var hooker = require(‘hooker‘); if (gruntConfig.mochaTest.testReporter && gruntConfig.mochaTest.testReporter.options.output) { var output = gruntConfig.mochaTest.testReporter.options.output; var fs = require(‘fs‘); fs.open(output, "w"); hooker.hook(process.stdout, ‘write‘, { pre: function (result) { fs.open(output, "a", 420, function (e, fd) { if (e) throw e; fs.write(fd, result, 0, ‘utf8‘, function (e) { if (e) throw e; fs.closeSync(fd); }) }); } }); }; //用例头文件 加载 var _compile = module.__proto__._compile; module.__proto__._compile = function (content, filename) { if (filename.indexOf(‘tdd_api_testcase‘) > -1 || filename.indexOf(‘bdd_api_testcase‘) > -1) { content = grunt.file.read(‘case_header.js‘) + content; } _compile.call(this, content, filename); };
环境准备
现下在相关的可执行程序并安装,它们是
Python 2.7.6 (jsdom依赖)
jscoverage0.5.1 (grunt-jscoverage依赖)
并把它们的路径添加到系统环境变量中
Package.json中维护如下依赖的插件
"devDependencies": { "grunt": "^0.4.4", "grunt-cli": "^0.1.13", "jsdom": "^0.10.3", "requirejs": "^2.1.11", "grunt-jscoverage": "0.0.3", "grunt-mocha-test": "^0.10.0", "hooker": "^0.2.3", "blanket": "^1.1.6" }