自己的对于测试的理解很少,在做前端的时候,基本上很少写测试,前端的测试很难进行,首先最简单的接口测试,尝试对一个接口的回溯数据进行mock,对各类型的数据返回进行测试。还有一种是前端页面行为的测试,比如对于各种步骤的模拟点击。这个是非常难进行的。主要是用户的行为千差万别,很难真正的做到全量的测试,这也是前端自动化测试的难处。无法全量的测试就有可能让测试变得没有意义。
再者前端的测试可能还和页面展示有关。比如位置错乱,这个在不同分辨率和不同浏览器的展示上可能不同。这个需要更多只管的去看,很难用自动化的方式去做。当然现在也可以通过快照的方式来看。但是这并不能得到前端开发的信任。所以以前对于前端开发时就没有测试这个环节。
揭开前端的遮羞布,拥抱测试,是自己的产品具备良好的质量保证。
测试包含单元测试、性能测试、安全测试和功能测试几方面。
单元测试
自我测试会一定程度上保证自己软件的质量,自己开发出来的产品一定对自己的产品负责。要产出高质量的产品必须要花费更多的精力。单元测试会再早期时多花费一定的成本。但这个成本圆圆低于后期长期的维护投入。如果一段代码写完无法为其写出良好的单元测试时,这段代码一定有可以考究的地方,好的代码的单元测试必然是轻量的。
- 单一职责:将代码功能解耦,每段代码的职责相对单一。
- 接口抽象:通过对程序代码进行接口抽象后,我们可以针对接口进行测试。
- 层次分离:分离一段代码的层级逻辑,典型的如 MVC。
单元测试包括:断言、测试框架、测试用例、测试覆盖率、mock、持续集成。
断言
断言用于检查程序在运行时是否满足期望。都引用的是assert库。
- assert.fail(actual, expected, message, operator);直接抛出错误;actual 实际值,expected 期望值,message 消息,operator 分隔符
assert.fail('karyn', 'song', '名字匹配错误', '||') // AssertionError: 名字匹配错误
- assert(value, message) assert.ok(value, [message]) assert.equal(actual, expected, [message]);三个是相同的意思,断言真
function add (a, b) {
return a + b;
}
var expected = add(1,2);
assert( expected === 3, '计算出错'); // 无
assert( expected === 4, '计算出错'); // AssertionError: 计算出错
assert.equal(expected, 3, '计算出错'); // 无
- assert.notEqual(actual, expected, [message]);断言不等
function add (a, b) {
return a + b;
}
var expected = add(1,2);
assert(expected != 4, '计算出错'); // 无
assert.ok(expected != 4, '计算出错'); // AssertionError: 计算出错
assert.notEqual(expected, 4, '计算出错'); // 无
- assert.deepEqual(actual, expected, [message]);深度匹配,不再是简单的参数
var list1 = [1, 2, 3, 4, 5],
list2 = [1, 2, 3, 4, 5];
assert.deepEqual(list1, list2, '两个数据不相同'); // 无
var person1 = { "name":"john", "age":"22" };
var person2 = { "name":"john", "age":"21" };
assert.deepEqual(person1, person2, '两个数据不相同'); // 两个数据不相同
- assert.notDeepEqual(actual, expected, [message]);和上面的用法相同,结果相反
- assert.strictEqual(actual, expected, [message]);严格相等。在
javascript中就是三个等号的意思,因为javascript是弱类型语言。所以1 == '1'但1 !== '1'
assert.strictEqual(1, '1', '不全等'); // 不全等
- assert.notStrictEqual(actual, expected, [message]);和上面的用法相同,结果相反
- assert.throws(block, [error], [message]);判断是否会抛错
assert.throws(
function() {
throw new Error("Wrong value");
},
'没有抛出错误'
); // 无
assert.throws(
function() {
// throw new Error("Wrong value");
},
'没有抛出错误'
); // Missing expected exception. 没有抛出错误
- assert.doesNotThrow(block, [message]);与上面用法相同,结果相反
- assert.ifError(value);测试值是否不为 false,当为 true 时抛出。常用于回调中第一个 error 参数的检查。
function sayHello(name, callback) {
var error = true;
var str = "Hello " + name;
callback(error, str);
}
sayHello('World', function(err, value) {
assert.ifError(err);
assert.equal(value, "Hello World");
}) // true
测试框架
断言一旦失败就会被迫退出,当断言大量一起用时,就不那么友好了,测试框架其中一个功能就是用于此处。测试框架用于为测试服务,它本身不参与测试,主要用于管理测试用例和生成测试报告。提升测试用例的开发速度,提高测试用例的可维护行和可读性。其中一个框架是:mocha
测试用例的不同组织方式称为测试风格,主流的单元测试风格主要由TDD(测试驱动开发)和BDD(行为驱动开发)。区别在于关注点不同和表达方式不同。
首先安装mocha:npm install -g mocha
bdd 写法:
var assert = require("assert");
describe('type', function(){
// 钩子
beforeEach(function(){
console.log('beforeEach type')
})
before(function(){
console.log('before type')
})
before(function(){
console.log('before type second time')
})
after(function(){
console.log('after type')
})
// 测试主体
describe('#typeof()', function(){
it('typeof 应该返回正确的值类型', function(){
assert.equal('string', typeof('a'));
assert.equal('string', typeof([]));
})
})
// 异步测试主体
describe('#readFile()', function(){
it('读取文件 xxx.js 不能出错', function(done){
fs.readFile('xxx.js', function(err){
assert.ifError(err);
done();
});
})
})
it('should read test.js without error', function(done){
it('读取文件 xxxa.js 不能出错', function(done){
fs.readFile('xxxa.js', function(err){
assert.ifError(err);
done();
});
})
})
})
// 执行命令
mocha -u bdd test.js
// 输出
type
before type
before type second time
#typeof()
beforeEach type
1) typeof 应该返回正确的值类型
#readFile()
beforeEach type
✓ 读取文件 xxx.js 不能出错
#readFile()
beforeEach type
2) 读取文件 xxxa.js 不能出错
after type
1 passing (15ms)
2 failing
1) type #typeof() typeof 应该返回正确的值类型:
AssertionError: 'string' == 'object'
+ expected - actual
-string
+object
at Context.<anonymous> (test.js:25:20)
2) type #readFile() 读取文件 xxxa.js 不能出错:
Uncaught Error: ENOENT, open 'xxxa.js'
tdd 写法:
var assert = require("assert"),
fs = require('fs');
suite('type', function(){
setup(function(){
console.log('setup type')
})
teardown(function(){
console.log('teardown type')
})
suite('#typeof()', function(){
test('typeof 应该返回正确的值类型', function(){
assert.equal('string', typeof('a'));
assert.equal('string', typeof([]));
})
})
suite('#readFile()', function(){
test('读取文件 xxx.js 不能出错', function(done){
fs.readFile('xxx.js', function(err){
assert.ifError(err);
done();
});
})
})
suite('#readFile()', function(){
test('读取文件 xxxa.js 不能出错', function(done){
fs.readFile('xxxa.js', function(err){
assert.ifError(err);
done();
});
})
})
});
// 执行命令
mocha -u tdd test.js
// 输出
type
before type
before type second time
#typeof()
beforeEach type
1) typeof 应该返回正确的值类型
#readFile()
beforeEach type
✓ 读取文件 xxx.js 不能出错
#readFile()
beforeEach type
2) 读取文件 xxxa.js 不能出错
after type
1 passing (15ms)
2 failing
1) type #typeof() typeof 应该返回正确的值类型:
AssertionError: 'string' == 'object'
+ expected - actual
-string
+object
at Context.<anonymous> (test.js:25:20)
2) type #readFile() 读取文件 xxxa.js 不能出错:
Uncaught Error: ENOENT, open 'xxxa.js'
测试用例
一个行为或者功能要由完善的、多方面的测试用例,一个测试用例中包含至少一个断言。测试用例最少需要通过正反两个测试来保证对功能的覆盖。这是最基本的测试用例。异步代码还需要关注超时方面。
超时关注:
var assert = require("assert");
describe('timeout', function(){
this.timeout(500);
it('必须在500毫秒内完成', function(done){
setTimeout(done)
})
it('必须在500毫秒内完成', function(done){
setTimeout(done, 1000)
})
});
timeout
✓ 必须在500毫秒内完成
1) 必须在500毫秒内完成
1 passing (514ms)
1 failing
1) timeout 必须在500毫秒内完成:
Error: timeout of 500ms exceeded. Ensure the done() callback is being called in this test.
测试覆盖率
测试覆盖率是单元测试中的一个重要指标,它能够概括性地给出整体的覆盖度,也能明确地给出统计到行的覆盖情况。测试覆盖率模块:istanbul。之后准备将所有单元测试串起来再加入这块东西。
mock
大多异常与输入数据并没有。比如数据库的异步调用,除了输入异常外,还有可能是网络异常、权限异常等非数据输入相关的情况。这种情况我们就可以使用mock的方式来模拟不容易发生的情况。mock模块推荐使用muk。
var assert = require("assert"),
fs = require('fs'),
muk = require('muk');
describe('mock', function(){
beforeEach(function(){
muk(fs, 'readFile', function(path, encoding){
throw new Error('网络错误导致读取文件出错');
})
})
it('读取文件 xxx.js 不能出错', function(done){
fs.readFile('xxx.js', function(err){
assert.ifError(err);
done();
});
})
after(function(){
muk.restore()
})
});
// 执行
mocha -u bdd test.js
// 执行结果(本来应该可以通过的测试,经过mock,还是抛出了预定的错误)
mock
1) 读取文件 xxx.js 不能出错
0 passing (12ms)
1 failing
1) mock 读取文件 xxx.js 不能出错:
Error: 网络错误导致读取文件出错
at Object.readFile (test.js:8:19)
at Context.<anonymous> (test.js:12:12)
私有方法
对于node而言,又有一个难点会出现在单元测试的过程中,那就是私有方法的测试。只有挂载在exports或者mudule.exports上的变量或方法才可以被外部通过require的方式引用。那这类方法怎么办呢?
// xxx.js
function getType(obj){
return Object.prototype.toString.call(obj).slice(8,-1)
}
// test.js
var assert = require("assert"),
rewire = require('rewire');
describe('mock', function(){
var lib = rewire('./xxx.js'),
getType = lib.__get__('getType');
assert.equal('Array', getType([]));
});
// 执行
mocha -u bdd test.js
// 执行结果
0 passing (2ms)
工程化
前面已经大概说了所有的测试设计的内容。接下来希望将这个开发面向工程化。
自己写了一个工程化的单元测试例子,单元测试用例,基本上将上面说的单元测试都串起来了。
自动化
我们在把代码上传到github时,程序可以自动帮我们跑测试,并且会给出相应的反馈。
有同学写了相关教程,请戳这个教程,由于(https://travis-ci.org/)打开太慢就放弃了
性能测试
单元测试主要用于检测代码的行为是否符合预期。性能检测是对已有功能是否满足生产环境的性能要求,能否承担实际业务带来的压力。性能测试的范畴包括:负载测试、压力测试和基准测试等。
基准测试
基准测试就是统计在多少时间执行了多少次某个方法。为了增强可比性,一般会找一个参照物。
比如比较Array.prototype.map和单纯的for循环取值
var run = function(name, times, fn, arr, callback) {
var start = (new Date()).getTime();
for (var i = 0; i < times; i++) {
fn(arr, callback);
}
var end = (new Date()).getTime();
console.log('Runing %s %d times cost %d ms', name, times, end - start);
}
function callback(item) {
return item;
}
function nativeMap(arr, callback) {
return arr.map(callback);
}
function customMap(arr, callback) {
var ret = [];
for (var i = 0; i < arr.length; i++) {
ret.push(callback(arr[i], i, arr))
}
return ret;
}
run('nativeMap', 1000000, nativeMap, [0, 1, 2, 3, 4, 5, 6], callback)
run('customMap', 1000000, customMap, [0, 1, 2, 3, 4, 5, 6], callback)
// 运行结果
Runing nativeMap 1000000 times cost 492 ms
Runing customMap 1000000 times cost 48 ms
引入模块benchmark。他实现的基本测试会就多个取样然后求得方差。得到相应的结果。
var Benchmark = require('benchmark'),
suite = new Benchmark.Suite();
var arr = [0, 1, 2, 3, 5, 6];
function callback(item) {
return item;
}
suite.add('nativeMap', function() {
return arr.map(callback);
}).add('customMap', function() {
var ret = [];
for (var i = 0; i < arr.length; i++) {
ret.push(callback(arr[i]));
}
return ret
}).on('cycle', function(event) {
console.log(String(event.target));
}).on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').pluck('name'));
}).run();
// 运行结果
nativeMap x 1,989,333 ops/sec ±1.34% (90 runs sampled)
customMap x 20,825,754 ops/sec ±1.85% (88 runs sampled)
Fastest is customMap
压力测试
除了可以对基本的方法进行基准测试外,通常还会对网络接口进行压力测试以判断网络接口的性能。考核的指标有响应时间,吞吐量,并发数等。可以使用ab、siege、http_load等工具。可以自我测试服务器抗压能力。多核利用能一定程度上提高QPS
node调试
补充一个调试工具,之前自己的调试方式竟然是console.log,提供一种界面debugger的方式。
- 全局安装 Inspector,npm install -g node-inspector
- 在代码中打好 debugger 准备调试
- 运行调试代码 node debug debugger.js
- 在另一个 bash 中开始界面工具, node-inspector
- 访问(调试界面)