(二)Mocha源碼閱讀: 測試執行流程一之引入用例node
測試流程我把它分爲引入用例和執行用例兩部分。引入用例就是把須要執行的用例函數歸歸類,命好名字。執行用例就是執行這些函數並收集結果。es6
在加載用例文件前的準備階段。 上一篇咱們看到最後調用了mocha.run()觸發了流程,run也就是入口了。但run以前先來看一下Mocha的constructor。github
// 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。npm
// 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來分析:api
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導出的functionbash
//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.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到這裏就結束了,能夠看到作的工做就是給全局綁定上各類鉤子和其它方法。
// 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內部的
// 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。
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的內容取出來
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用例流程算講完了。 咱們總結一下大概是