(二)Mocha源碼閱讀: 測試執行流程一之引入用例

(一)Mocha源碼閱讀: 項目結構及命令行啓動html

(二)Mocha源碼閱讀: 測試執行流程一之引入用例node

(三)Mocha源碼閱讀: 測試執行流程一執行用例git

介紹

測試流程我把它分爲引入用例和執行用例兩部分。引入用例就是把須要執行的用例函數歸歸類,命好名字。執行用例就是執行這些函數並收集結果。es6

正文

1. pre-require

在加載用例文件前的準備階段。 上一篇咱們看到最後調用了mocha.run()觸發了流程,run也就是入口了。但run以前先來看一下Mocha的constructor。github

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。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.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用例流程算講完了。 咱們總結一下大概是

  1. 在global中加入些全局函數如describe, it
  2. 依次require文件
  3. 文件加載運行時調用describe, it
  4. 生成suite數據結構 Suite: { suites: [{ //suite tests: [{ // test }] }] }
相關文章
相關標籤/搜索