测试

博客分类: 原创

测试

自己的对于测试的理解很少,在做前端的时候,基本上很少写测试,前端的测试很难进行,首先最简单的接口测试,尝试对一个接口的回溯数据进行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(行为驱动开发)。区别在于关注点不同和表达方式不同。

首先安装mochanpm 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

压力测试

除了可以对基本的方法进行基准测试外,通常还会对网络接口进行压力测试以判断网络接口的性能。考核的指标有响应时间,吞吐量,并发数等。可以使用absiegehttp_load等工具。可以自我测试服务器抗压能力。多核利用能一定程度上提高QPS

node调试

补充一个调试工具,之前自己的调试方式竟然是console.log,提供一种界面debugger的方式。

  • 全局安装 Inspector,npm install -g node-inspector
  • 在代码中打好 debugger 准备调试
  • 运行调试代码 node debug debugger.js
  • 在另一个 bash 中开始界面工具, node-inspector
  • 访问(调试界面