介绍
测试流程我把它分为引入用例和执行用例两部分。引入用例就是把需要执行的用例函数归归类,命好名字。执行用例就是执行这些函数并收集结果。
正文
1. pre-require
在加载用例文件前的准备阶段。 上一篇我们看到最后调用了mocha.run()触发了流程,run也就是入口了。但run之前先来看一下Mocha的constructor。
constructor
// lib/mocha.js
function Mocha(options) {
...
// suite就是包含用例test的一个组,suite可以嵌套suite
this.suite = new exports.Suite('', new exports.Context());
this.ui(options.ui);
...
this.reporter(options.reporter, options.reporterOptions);
...
}
复制代码
this.suite是Suite类new出来,第一个参数为空字符串的suite,下面看Suite的构造函数得知 this.suite就是根suite。
// lib/suite.js
function Suite(title, parentContex){
...
this.title = title;
this.root = !title;
...
}
复制代码
回到mocha.js,下面调用了this.ui(options.ui)。Mocha提供了几种编写测试用例的风格bdd|tdd|qunit|exports, 这几种风格只是写法上有所区别, 这里以默认的bdd来分析:
Mocha.prototype.ui = function(name) {
name = name || 'bdd';
// exports.interfaces是require interface文件夹下的几个js文件的模块, exports.interfaces[name]同this._ui就是其中一个模块默认bdd,
由于调用了this._ui(this.suite)可以知道bdd导出了一个function
this._ui = exports.interfaces[name];
if (!this._ui) {
try {
this._ui = require(name);
} catch (err) {
throw new Error('invalid interface "' + name + '"');
}
}
this._ui = this._ui(this.suite);
this.suite.on('pre-require', function(context) {
exports.afterEach = context.afterEach || context.teardown;
exports.after = context.after || context.suiteTeardown;
exports.beforeEach = context.beforeEach || context.setup;
exports.before = context.before || context.suiteSetup;
exports.describe = context.describe || context.suite;
...
});
return this;
};
复制代码
bdd导出的function
//lib/interfaces/bdd.js
module.exports = function bddInterface(suite) {
// suite就是传过来的根suite
var suites = [suite];
suite.on('pre-require', function(context, file, mocha) {
...
});
};
复制代码
函数主要监听了suite的pre-require事件,我们在这里还有mocha.ui里面都看到监听了这个事件。它其实就是一个订阅发布模式,我们看Suite是如何实现它的:
// lib/suite.js
var EventEmitter = require('events').EventEmitter;
...
inherits(Suite, EventEmitter);
复制代码
直接继承了events模块的EventEmitter,inherits就是调用的node util模块的inherits
如果看node关于util.inherits的文档, 可以发现它是不推荐用这方法而是推荐es6的class和extends。原因在这里, 我总结一下大意是inherits的实现是通过Object.setPrototypeOf(ctor.prototype, superCtor.prototype);这样存在一个情况是如果用isPrototypeOf来判断子类和父类的constructor是会返回false的, 也就是ctor.isPrototypeOf(superCtor) === false. 而用class extends出来的是true
回到bdd.js我们根据这个on可以推测出有什么地方肯定调用了suite.emit('pre-require')从而触发了这个回调,回调的内容我们后面看
我们再往回到mocha.ui发现监听完pre-require事件后就执行完了,再往上到Mocha的constructor,可以看到执行了this.reporter()
var reporters = require('./reporters');
...
Mocha.prototype.reporter = function(reporter, reporterOptions) {
//代码很容易看懂,都贴出来是对这段代码所体现出的设计完整性,友好性和错误提示很值得借鉴。
if (typeof reporter === 'function') {
this._reporter = reporter;
} else {
reporter = reporter || 'spec';
var _reporter;
// Try to load a built-in reporter.
if (reporters[reporter]) {
_reporter = reporters[reporter];
}
// Try to load reporters from process.cwd() and node_modules
if (!_reporter) {
try {
_reporter = require(reporter);
} catch (err) {
if (err.message.indexOf('Cannot find module') !== -1) {
// Try to load reporters from a path (absolute or relative)
try {
_reporter = require(path.resolve(process.cwd(), reporter));
} catch (_err) {
err.message.indexOf('Cannot find module') !== -1
? console.warn('"' + reporter + '" reporter not found')
: console.warn(
'"' +
reporter +
'" reporter blew up with error:\n' +
err.stack
);
}
} else {
console.warn(
'"' + reporter + '" reporter blew up with error:\n' + err.stack
);
}
}
}
if (!_reporter && reporter === 'teamcity') {
console.warn(
'The Teamcity reporter was moved to a package named ' +
'mocha-teamcity-reporter ' +
'(https://npmjs.org/package/mocha-teamcity-reporter).'
);
}
if (!_reporter) {
throw new Error('invalid reporter "' + reporter + '"');
}
this._reporter = _reporter;
}
this.options.reporterOptions = reporterOptions;
return this;
};
复制代码
reporters是在lib/reporters引入的一堆reporter function,这里会匹配一个存入this._reporter。
constructor里面值得分析的就这么多,下面开始run
mocha.run
Mocha.prototype.run = function(fn) {
if (this.files.length) {
this.loadFiles();
}
...
};
复制代码
this.files在Mocha实例化当中没有看到赋值,它的赋值是在bin中做的,上一篇可以看到, 是找到所有files后 直接mocha.files=files。这里似乎有一点偷懒, mocha很多option的赋值都是个function比如:
Mocha.prototype.invert = function() {
this.options.invert = true;
return this;
};
复制代码
这个files虽然不是属于实例下什么子属性的,但直接赋值看起来有些突兀的。
回到run,调用了loadFiles
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();
};
复制代码
这里我们终于看到每个文件都依次从根suite触发了emit的'pre-require', 这一emit标志着流程的开始。刚才我们在lib/interfaces/bdd.js中看到一个绑定pre-require的,这个也是我们能直接调用describe, it的关键
//lib/interfaces/bdd.js
module.exports = function bddInterface(suite) {
var suites = [suite];
suite.on('pre-require', function(context, file, mocha) {
var common = require('./common')(suites, context, mocha);
...
});
};
复制代码
首先我们就看看这个'./common'返回的函数做了什么
module.exports = function(suites, context, mocha) {
return {
runWithSuite: function runWithSuite(suite) {
...
},
before: function(name, fn) {
...
},
after: function(name, fn) {
...
},
beforeEach: function(name, fn) {
...
},
afterEach: function(name, fn) {
...
},
suite: {
only: function only(opts) {
...
},
skip: function skip(opts) {
...
},
create: function create(opts) {
...
}
},
test: {
...
}
};
};
复制代码
它返回一个包含很多函数的对象,而之所以这么返回应该是为了保存传进来的三个参数,使得这些函数可以调用
//lib/interfaces/bdd.js
module.exports = function bddInterface(suite) {
var suites = [suite];
suite.on('pre-require', function(context, file, mocha) {
var common = require('./common')(suites, context, mocha);
给context加了一些钩子函数
context.before = common.before;
context.after = common.after;
context.beforeEach = common.beforeEach;
context.afterEach = common.afterEach;
context.run = mocha.options.delay && common.runWithSuite(suite);
...
});
};
复制代码
//lib/interfaces/bdd.js
module.exports = function bddInterface(suite) {
var suites = [suite];
suite.on('pre-require', function(context, file, mocha) {
...
context.afterEach = common.afterEach;
context.run = mocha.options.delay && common.runWithSuite(suite);
// 给context绑定了写用例包含的describe, it, skip等
context.describe = context.context = function(title, fn) {
...
};
context.xdescribe = context.xcontext = context.describe.skip = function(
title,
fn
) {
...
};
context.describe.only = function(title, fn) {
...
};
context.it = context.specify = function(title, fn) {
...
};
context.it.only = function(title, fn) {
...
};
context.xit = context.xspecify = context.it.skip = function(title) {
...
};
context.it.retries = function(n) {
...
};
});
};
复制代码
这里这个context我们由emit看知道是global对象,这个对象如果在node下相当于browser下的window,作用就是我们现在给global加了这些属性,后面的程序执行时就可以直接调用相当于全局的属性和方法。 这也是为什么测试文件可以不用引入就直接写describe, it等。 由此也可以得出测试文件还没有执行,他们执行时就会走到这里绑定的函数里。 pre-require到这里就结束了,可以看到做的工作就是给全局绑定上各种钩子和其它方法。
2. require test
// lib/mocha.js
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'结束后,接下来的两个事件'require'和'post-require'我在框架中并没有找到监听者。不过这里比较有用的是在'require'中调用了require(file)。也就是从这里开始每个文件依次require测试用例。
// some test file written by user
describe('suite1', function(){
describe('suite2', function(){
it('test title', function(){
// test
})
})
})
复制代码
假定一个测试文件是这样的结构。 可以看到我们并不需要这些文件export什么东西,它们只要调用了describe, it等在global绑定的函数,就会走到mocha的内部。而让它们执行只需要require就可以了。 下面看这些用例是如何存储到mocha内部的
describe
// lib/interfaces/bdd.js
...
context.describe = context.context = function(title, fn) {
//  title此处上例的为suite1, fn是嵌套的describe, it等
return common.suite.create({
title: title,
file: file,
fn: fn
});
};
...
复制代码
这里回到bdd.js可以看到是调了common.suite.create
...
create: function create(opts) {
var suite = Suite.create(suites[0], opts.title);
...
}
...
复制代码
上面讲到common通过把一些函数放到对象返回的形式保存了三个参数, suites是其中一个,我们以上面写的测试文件为例。opts.title就是第一个describe调用'suite1' suites暂时只有一个suite,就是根suite,下面看Suite.create
// lib/suite.js
...
exports.create = function(parent, title) {
var suite = new Suite(title, parent.ctx);
suite.parent = parent;
title = suite.fullTitle(); // 这一行好像没什么用。。
parent.addSuite(suite);
return suite;
};
...
复制代码
new基本上初始化了一堆实例属性, suite.parent指回了根suite, 然后parent也就是根suite调用了addSuite
Suite.prototype.addSuite = function(suite) {
suite.parent = this;
suite.timeout(this.timeout());
//把parent的option如timeout, retries等设置到child suite上
suite.retries(this.retries());
suite.enableTimeouts(this.enableTimeouts());
suite.slow(this.slow());
suite.bail(this.bail());
this.suites.push(suite);
this.emit('suite', suite);
return this;
};
复制代码
这里注意两个: suite.parent = this其实和create当中suite.parent = parent是重复的。。 this.suites.push(suite),把子suite也就是title为suite1的suite放到根suite的suites里。 再回到common.suite.create:
// lib/interfaces/common.js
...
create: function create(opts) {
var suite = Suite.create(suites[0], opts.title);
suite.pending = Boolean(opts.pending);
suite.file = opts.file;
suites.unshift(suite);
...
}
...
复制代码
新创建的suite放在了suites的第一个
create: function create(opts) {
var suite = Suite.create(suites[0], opts.title);
...
suites.unshift(suite);
...
if (typeof opts.fn === 'function') {
opts.fn.call(suite);
suites.shift();
} ...
return suite;
}
复制代码
opts.fn就是我们调用describe时传到第二个参数的方法,这里对方法进行了调用, this指向包含的suite。
- 注意: 一般我们的方法里面可能会调用describe创建子suite或调用it方法创建test用例,如果是describe的话相当于递归调用了create方法, 而此时suites由于unshift了新创建的suite,suites[0]就是新创建的suite, 这样递归调用中创建的child suite的parent就正确的指向了父suite而不是根suite。 it后面再看。 由于describe和it都是同步运行,所以如果有嵌套,suites.shift()会一直压在后面,这样一层层运行,等子suite和test运行完, suites就把suite shift走了。
create: function create(opts) {
...
if (typeof opts.fn === 'function') {
...
} else if (typeof opts.fn === 'undefined' && !suite.pending) {
throw new Error(
'Suite "' +
suite.fullTitle() +
'" was defined but no callback was supplied. Supply a callback or explicitly skip the suite.'
);
} else if (!opts.fn && suite.pending) {
suites.shift();
}
return suite;
}
复制代码
后面两个判断是针对suite跳过的情况。
以上是创建suite,再看创建test用到的it
context.it = context.specify = function(title, fn) {
var suite = suites[0];
...
var test = new Test(title, fn);
test.file = file;
suite.addTest(test);
return test;
};
复制代码
这里的suites其实就是传入common中的suites, 这里我们考虑在describe调用时调用了it, 则suites[0]就是父suite, 然后new了一个Test, 调用父suite的addTest方法。 先看new的Test
// lib/test.js
function Test(title, fn) {
if (!isString(title)) {
throw new Error(
'Test `title` should be a "string" but "' +
typeof title +
'" was given instead.'
);
}
Runnable.call(this, title, fn);
this.pending = !fn;
this.type = 'test';
}
utils.inherits(Test, Runnable);
复制代码
Test通过utils.inherits(上文讲过)继承了Runnable类的原型,然后在构造函数中Runnable.call(this, title, fn)获得实例属性。此外Test还包含一个clone的原型方法,暂且不表。 那么Runnable是什么呢?为什么Test要继承它? Runnable字面看就是能运行的, test是包含了很多条语句的函数,而Mocha运行时还提供了很多钩子函数也就是Hook, hook同样也是些能运行的语句所以也会继承Runnable。 再来看Runnable
// lib/runnable.js
function Runnable(title, fn) {
this.title = title;
this.fn = fn;
this.body = (fn || '').toString();
this.async = fn && fn.length;
this.sync = !this.async;
this._timeout = 2000;
this._slow = 75;
...
}
utils.inherits(Runnable, EventEmitter);
复制代码
这里fn.toString()可以把一个function的内容取出来
- this.async = fn && fn.length 可以说是一个黑科技了 Mocha在test中提供了异步调用的形式如 describe('async', it('async test', function(done){ // 异步 setTimeout(done, 1000) })) describe('sync', it('async test', function(){ // 同步 })) 如果用户调用done就算异步,done回调会等待调用后来知晓用例运行结束 那么我们是怎么知道里面的回调是否用了done呢?就是通过fn.length, 回调中接收几个参数fn.length就是几, 所以如果参数中接收了done, length就为1, async就为true。
Runnable还有很多其它原型方法,暂时先不需要管
回到it方法,下面调用了suite.addTest(test)
// lib/suite.js
Suite.prototype.addTest = function(test) {
test.parent = this;
test.timeout(this.timeout());
test.retries(this.retries());
test.enableTimeouts(this.enableTimeouts());
test.slow(this.slow());
test.ctx = this.ctx;
this.tests.push(test);
this.emit('test', test);
return this;
};
复制代码
这里主要把suite的配置和ctx执行上下文赋给了test, 同时把它放入了自己的tests中。
至此,主要的引入test用例流程算讲完了。 我们总结一下大概是
- 在global中加入些全局函数如describe, it
- 依次require文件
- 文件加载运行时调用describe, it
- 生成suite数据结构 Suite: { suites: [{ //suite tests: [{ // test }] }] }