分析前端单元测试工具 Mocha, 了解它是怎么进行TDD和BDD的。
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
断言表示一些布尔表达式,在编写代码的时候,我们总是会作出一些假设,断言就是用于代码中捕捉这些假设。 在单元测试中,我们经常使用断言来验证我们的代码是否正常运行。断言可以有两种形式
assert Expresstion1
assert Expresstion1:Expresstion2
其中Expression1应该总是一个布尔值,Expression2是断言失败时,输出的失败消息的字符串。
测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD的基本思路就是通过测试来推动整个开发的进行,但测试驱动开发并不只是单纯的测试工作,而是把需求分析,设计,质量控制量化的过程。TDD首先考虑使用需求(对象、功能、过程、接口等),主要是编写测试用例框架对功能的过程和接口进行设计,而测试框架可以持续进行验证。
行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、 QA和非技术人员或商业参与者之间的协作。主要是从用户的需求出发,强调系统行为。BDD最初是由Dan North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。
Mocha.js是被广泛使用的Javascript测试框架,在浏览器和Node环境都可以使用。Mocha提供TDD和BDD的测试接口。 Mocha提供了
在Mocha中运行你使用任何断言库来进行代码的测试,其中有 | 库 | 描述 | | :-------------- | :-------------------------------------------------------- | | should.js | BDD风格的测试接口 | | expect.js | expect()风格的断言 | | chai | 提供expect(),assert()和should这几种风格的断言 | | better-assert | C语言风格的断言 | | unexpected | BDD断言的扩展 | 当然,我们可以用Nodejs内建的assert模块来进行断言。
describe("测试数组", function(){
it("测试indexOf()", function(){
assert.equal(-1, [1,2,3].indexOf(4));
});
});
TDD测试风格的接口:
BDD测试风格的接口
首先,mocha的默认模式是BDD,我们以BDD为例看一下mocha是怎么运行的: 这个是要测试的源文件,传入两个数字,返回和
// add.js
export default (a, b) => a + b;
这个是测试的文件,用了chai这个断言库,expect(add(1, 1)).to.be.equal(2) 是断言部分
// test.js
import add from "./add"
import { expect } from "chai"
describe('Test Start',() => {
describe('加法函数测试',() => {
it('1 + 1 等于 2', () => expect(add(1, 1)).to.be.equal(2))
it('返回值是Number',() => expect(add(1, 1)).to.be.a('number'))
})
})
// index.js
require("babel-register")({
presets: [
"es2015",
"stage-0"
],
plugins: ["transform-async-to-generator","transform-runtime"]
});
require('./test');
命令行运行结果:
首先,Mocha的命令行接口是用 commander 写的,在/bin/_mocha文件中可以看到。默认ui是bdd,默认文件是test/*
// bin/_mocha
program
...
.option('-u, --ui <name>', 'specify user-interface (' + interfaceNames.join('|') + ')', 'bdd')
...
var args = program.args;
// default files to test/*.{js,coffee}
if (!args.length) {
args.push('test');
}
args.forEach(function (arg) {
var newFiles;
try {
newFiles = utils.lookupFiles(arg, extensions, program.recursive);
} catch (err) {
...
}
files = files.concat(newFiles);
});
...
mocha.files = files;
runner = mocha.run(program.exit ? exit : exitLater);
...
// lib/mocha.js
Mocha.prototype.run = function (fn) {
if (this.files.length) {
this.loadFiles();
}
...
return runner.run(done);
};
其中loadFiles是用来加载文件的,我们看一下他的源码:
// lib/mocha.j
Mocha.prototype.loadFiles = function (fn) {
var self = this;
var suite = this.suite;
this.files.forEach(function (file) {
file = path.resolve(file);
suite.emit('pre-require', global, file, self);
suite.emit('require', require(file), file, self);
suite.emit('post-require', global, file, self);
});
fn && fn();
};
这里通过emit触发了pre-require事件,那么pre-require事件在哪呢?
// lib/interfaces/bdd.js
suite.on('pre-require', function (context, file, mocha) {
var common = require('./common')(suites, context, mocha);
context.before = common.before;
context.after = common.after;
context.beforeEach = common.beforeEach;
context.afterEach = common.afterEach;
context.run = mocha.options.delay && common.runWithSuite(suite);
/**
* Describe a "suite" with the given `title`
* and callback `fn` containing nested suites
* and/or tests.
*/
context.describe = context.context = function (title, fn) {
...
};
...
context.it = context.specify = function (title, fn) {
...
};
});
其中context是emit时候传入的global对象,这段代码给global定义了一些属性,比如BDD模式的例子里的describe 和 it,传入的参数都是是title + fn。所以bdd.js这个文件的作用就是给全局对象注册bdd需要的一些接口。
Mocha.run 最后执行的是runner.run(), 那我们继续分析Runner
// lib/runner.js
Runner.prototype.run = function (fn) {
...
function start () {
self.started = true;
self.emit('start');
self.runSuite(rootSuite, function () {
debug('finished running');
self.emit('end');
});
}
debug('start');
...
return this;
};
这里,emit 触发 start事件,这个具体的处理在哪呢,我们全局搜索一下,发现在lib/reporters/下的每个文件基本都有on start事件,到底实际是触发的哪一个呢?我们继续分析一下,在Mocha目录下发现这么一段:
// lib/mocha.js
/**
* Set reporter to `reporter`, defaults to "spec".
* ...
*/
Mocha.prototype.reporter = function (reporter, reporterOptions) {
...
reporter = reporter || 'spec';
...
};
所以默认的报告格式是spec,我们去 lib/reporters/spec.js 看一看:
// lib/reporters/spec.js
function Spec (runner) {
...
runner.on('start', function () {
console.log();
});
runner.on('suite', function (suite) {
++indents;
console.log(color('suite', '%s%s'), indent(), suite.title);
});
...
runner.on('pass', function (test) {
var fmt;
if (test.speed === 'fast') {
fmt = indent() +
color('checkmark', ' ' + Base.symbols.ok) +
color('pass', ' %s');
console.log(fmt, test.title);
} else {
fmt = indent() +
color('checkmark', ' ' + Base.symbols.ok) +
color('pass', ' %s') +
color(test.speed, ' (%dms)');
console.log(fmt, test.title, test.duration);
}
});
runner.on('fail', function (test) {
console.log(indent() + color('fail', ' %d) %s'), ++n, test.title);
});
runner.on('end', self.epilogue.bind(self));
}
可以看到这里是响应一些事件的处理,回到Runner
// lib/runner.js
Runner.prototype.run = function (fn) {
...
function start () {
self.started = true;
self.emit('start');
self.runSuite(rootSuite, function () {
debug('finished running');
self.emit('end');
});
}
debug('start');
...
return this;
};
这样start对应的就是lib/reporters/spec.js里的 console.log(); 输出空行。我们继续往下走runSuite。
// lib/runner.js
Runner.prototype.runSuite = function (suite, fn) {
...
function next (errSuite) {
...
if (self._grep !== self._defaultGrep) {
Runner.immediately(function () {
self.runSuite(curr, next);
});
} else {
self.runSuite(curr, next);
}
}
function done (errSuite) {
...
self.hook('afterAll', function () {
self.emit('suite end', suite);
fn(errSuite);
});
}
}
...
this.hook('beforeAll', function (err) {
if (err) {
return done();
}
self.runTests(suite, next);
});
};
基本上是开始测试之前的准备,next作为回调函数不断执行下一个suite,我们再继续看看runTests:
// lib/runner.js
Runner.prototype.runTests = function (suite, fn) {
...
function next (err, errSuite) {
...
// next test
test = tests.shift();
...
self.hookDown('beforeEach', function (err, errSuite) {
self.currentRunnable = self.test;
self.runTest(function (err) {
...
self.emit('pass', test);
self.emit('test end', test);
self.hookUp('afterEach', next);
});
});
}
...
next();
};
这里是不断调用next,执行runTest,回调函数里触发了pass和test end事件,我们继续看runTest:
// lib/runner.js
Runner.prototype.runTest = function (fn) {
...
try {
test.on('error', function (err) {
self.fail(test, err);
});
test.run(fn);
} catch (err) {
fn(err);
}
};
终于到了runTest了,可以看到test运行在try catch中,如果抛出错误,捕获错误并传入回调函数。如果成功呢?我们需要知道test.run是个啥东西。
上面的过程中,不断提到Suite和Test,这两个东西到底是指啥呢?找源码太麻烦了,我们直接命令行输出一下看看,首先是suite:我在runSuite函数下console.log输出了一下Suite,结果是:
Suite {
title: '',
ctx: {},
suites:
[ Suite {
title: 'Test Start',
ctx: {},
suites: [Object],
tests: [],
...
file: '/Users/zhangruiwu/Desktop/demo-learn/newBlog/server/test-example/index.js' } ],
tests: [],
...
Suite {
title: 'Test Start',
ctx: {},
suites:
[ Suite {
title: '加法函数测试',
ctx: {},
suites: [],
tests: [Object],
...
Test Start
可以发现suite就是describe 生成的对象,我们测试文件中一共两个describe,Test Start和加法函数测试,两者是嵌套关系,在这里也体现出来了。我们继续分析Test: 把runSuite 加的console.log(suite) 改成console.log(suite.suites[0]),结果是:
Suite {
title: '加法函数测试',
ctx: {},
suites: [],
tests:
[ {
"title": "1 + 1 等于 2",
"body": "function () {\n return (0, _chai.expect)((0, _add2.default)(1, 1)).to.be.equal(2);\n }",
...
},
{
"title": "返回值是Number",
"body": "function () {\n return (0, _chai.expect)((0, _add2.default)(1, 1)).to.be.a('number');\n }",
...
} ],
...
可以发现Test就是it生成的对象。Suite 和 Test对应的文件是 lib/suite.js和 lib/test.js。我们找一下test.run ,test.js 里有这么一行
// lib/test.js
/**
* Inherit from `Runnable.prototype`.
*/
Test.prototype = create(Runnable.prototype, {
constructor: Test
});
我们再去Runnable看看:
// lib/runnable.js
Runnable.prototype.run = function (fn) {
...
// finished
function done (err) {
...
fn(err);
}
...
done();
...
};
很长我就直接贴最后的代码了。可以看到最后执行回调, 回调的内容在runTests里
####7. End
// lib/runner.js
self.runTest(function (err) {
...
self.emit('pass', test);
self.emit('test end', test);
self.hookUp('afterEach', next);
});
pass事件在lib/reporters/spec.js中:
// lib/reporters/spec.js
runner.on('pass', function (test) {
...
fmt = indent() +
color('checkmark', ' ' + Base.symbols.ok) +
color('pass', ' %s');
console.log(fmt, test.title);
...
});
其中Base.symbols.ok 代表通过时候显示的对号
// lib/reporters/base.js
exports.symbols = {
ok: '✓',
...
};
test end在lib/reporters/base.js中:
// lib/reporters/base.js
runner.on('test end', function () {
stats.tests = stats.tests || 0;
stats.tests++;
});
是用来统计test个数的。回到Runner
// lib/runner.js
Runner.prototype.run = function (fn) {
...
function start () {
self.started = true;
self.emit('start');
self.runSuite(rootSuite, function () {
debug('finished running');
self.emit('end');
});
}
debug('start');
...
return this;
};
发现runSuite的回调最终触发了end事件,在lib/reporters/spec.js中:
// lib/reporters/spec.js
runner.on('end', self.epilogue.bind(self));
self.epiloque在lib/reporters/base.js 中:
// lib/reporters/base.js
Base.prototype.epilogue = function () {
...
// passes
fmt = color('bright pass', ' ') +
color('green', ' %d passing') +
color('light', ' (%s)');
console.log(fmt,
stats.passes || 0,
ms(stats.duration));
...
console.log();
};
这就是最终输出的 2 passing (71ms) 了。 到这里我们的以例子做的分析结束,但是Mocha远远不止这些功能,篇幅所限更多的我们就不分析了。整体上看很复杂的面向对象编程,但是结构很清晰,功能划分明确,值得我们学习
对于 UI 自动化测试来说,许多所谓框架之间并没有太多差别,也从来不是影响整套测试用例是否健壮的关键性因素。相比之下,如何提高测试用例稳定性以及出现错误时 debug 的便捷性才是让 UI 自动化测试方案落地的重要细节。
自动化是科技行业前进的方向,但它也是一把双刃剑。做对了,能够削减费用开销,减少维护工作;做错了,会让流程更加复杂,并增加预算。就像任何技术或流程一样,自动化在某些时候、某些地方是有效的,而在其他一些领域
对更快交付高质量软件(或\\\\\\\"快速质量\\\\\\\")的需求要求组织以敏捷,持续集成(CI)和DevOps方法论来寻找解决方案。测试自动化是这些方面的重要组成部分。最新的《 2018-2019年世界质量报告》表明
在日常的开发中,整天赶需求的我们好像没有时间顾及自动化测试,尤其是在敏捷开发的时候。但其实自动化测试可以帮助我们提高代码和功能的健壮程度,大幅减少可能出现的bug。尤其是在复杂系统中
是否有很多人跟我一样有这样的一个烦恼,每天有写不完的需求、改不完的BUG,每天撸着重复、繁琐的业务代码,担心着自己的技术成长
一个webpack的api,通过执行require.context函数获取一个特定的上下文,主要用来实现自动化导入模块,在前端工程中,如果遇到从一个文件夹引入很多模块的情况,可以使用这个api,它会遍历文件夹中的指定文件,然后自动导入
随着自动化扩展到涵盖 IT 的更多方面,越来越多的管理员正在学习自动化技能并应用它们来减轻他们的工作量。自动化可以减轻重复性任务的负担,并为基础设施增加一定程度的一致性
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!