基于Grunt&mocha JavaScript自动化测试框架的实践

自动化测试的意义

以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();
        });
    });
});

从上面可以看出tdd下模块加载可以在setup中完成,只需要加载一次;而在bdd中模块在每个测试用例中都要重新加载。为了测试代码的简洁,以及测试用例的执行效率,个人偏向于TDD方式。

Jscoverage介绍

jscoverage的原理就是把测试代码每一个逻辑路径都插入一个检查点,当代码运行到那个路径的时候会在计数器里加1,多次运行那段的代码便一直累加上去。这里说一句Jscoverage的开源协议是GPL,使用前需要评估。

准备工作:

下载测试覆盖率的工具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

GruntFile.js文件中除定义基本的任务外,还需要添加如下代码,分别用来处理测试报告的生成和用例文件头文件加载。
//测试报告
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"
  }

总结 

相比于 上一篇 blog最大的 改进 就是 避免了对grunt的修改,同时一些 环境 准备 的工作单独 成了 一个步骤。使得代码的层次进一步的清晰










基于Grunt&mocha JavaScript自动化测试框架的实践,布布扣,bubuko.com

基于Grunt&mocha JavaScript自动化测试框架的实践

上一篇:编译运行java程序出现Exception in thread "main" java.lang.UnsupportedClassVersionError: M : Unsupported major.minor version 51.0


下一篇:JAVA与DOM解析器基础 学习笔记