自动化测试的意义
以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代码框架,完成相同的测试
|
|
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: 'webapp',
outputDirectory: 'webapp' + '_cov',
encoding: 'utf-8'
}
},
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"
}
总结