should.js源碼分析與學習

背景

爲了研究與學習某些測試框架的工作原理,同時也爲了完成培訓中實現一個簡單的測試框架的原因,我對should.js的代碼進行了學習與分析,現在與大家來進行交流下。

目錄

  • ext
  • assertion.js
  • assertion-error.js
  • config.js
  • should.js
  • util.js

其中ext爲文件夾,其餘爲js文件。

結構

其中should.js爲整個項目入口,asssertion.js爲should.js中的類,負責對測試信息進行記錄。assertion-error.js爲should.js定義了一個錯誤類,負責存儲錯誤信息。config.js中存儲了一些should.js中的一些配置信息。util.js中則定義了一些項目中常用的工具函數。

should.js

var should = function should(obj) {
    return (new should.Assertion(obj));
};

should.AssertionError = require('./assertion-error');
should.Assertion = require('./assertion');

should.format = util.format;
should.type = require('should-type');
should.util = util;
should.config = require('./config');

exports = module.exports = should;

should.js入口文件初始化了一個類,並將所有文件中其他的模塊進行引入。同時將自己export出去,讓自己能夠被require到。

should.extend = function (propertyName, proto) {
    propertyName = propertyName || 'should';
    proto = proto || Object.prototype;

var prevDescriptor = Object.getOwnPropertyDescriptor(proto, propertyName);

Object.defineProperty(proto, propertyName, {
    set: function () {
    },
    get: function () {
        return should(util.isWrapperType(this) ? this.valueOf() : this);
    },
    configurable: true
});

return {
    name: propertyName, descriptor: prevDescriptor, proto: proto};
};

should.js自身定義了一個extend方法,用於兼容should.js的另一種調用方式,即should(obj)的方式等於should.js的常規調用方式obj.should,從而兼容另一種寫法。

should
    .use(require('./ext/assert'))
    .use(require('./ext/chain'))
    .use(require('./ext/bool'))
    .use(require('./ext/number'))
    .use(require('./ext/eql'))
    .use(require('./ext/type'))
    .use(require('./ext/string'))
    .use(require('./ext/property'))
    .use(require('./ext/error'))
    .use(require('./ext/match'))
    .use(require('./ext/contain'));

should.js中還定義了use方法,從而讓我們能夠自己編寫一些類型判斷例如isNumber等函數導入到項目中,從而方便進行測試。項目目錄中的ext文件夾就是編寫的一些簡單的should.js的擴展。後面將在介紹擴展時對兩者的工作原理以及使用方法進行介紹。

assertion.js

function Assertion(obj) {
    this.obj = obj;

    //any標誌位
    //@type {boolean}
    this.anyOne = false;
    
    //not標誌位
    //@type {boolean}
    this.negate = false;

    this.params = {actual: obj};
}

assertion.js中定義了一個Assertion類,其中any爲should.js中的any方法的標誌位,而not則爲其not方法的標誌位。

Assertion.add = function(name, func) {
    var prop = {enumerable: true, configurable: true};

    prop.value = function() {
        var context = new Assertion(this.obj, this, name);
        context.anyOne = this.anyOne;

        try {
            func.apply(context, arguments);
        } catch(e) {
            //check for fail
            if(e instanceof AssertionError) {
                //negative fail
                if(this.negate) {
                    this.obj = context.obj;
                    this.negate = false;
                    return this;
                }

                if(context !== e.assertion) {
                    context.params.previous = e;
                }

                //positive fail
                context.negate = false;
                context.fail();
            }
            // throw if it is another exception
            throw e;
        }

        //negative pass
        if(this.negate) {
            context.negate = true;//because .fail will set negate
            context.params.details = 'false negative fail';
            context.fail();
        }

        //positive pass
        if(!this.params.operator) this.params = context.params;//shortcut
        this.obj = context.obj;
        this.negate = false;
        return this;
    };

    Object.defineProperty(Assertion.prototype, name, prop);
};

assertion.js中的add方法在Assertion的原型鏈中添加自定義命名的方法,從而讓我們能夠打包一些判斷的方法來進行調用,不需要重複進行代碼的編寫。該方法具體的使用方式我們在後面對擴展進行講解時將會提到。

Assertion.addChain = function(name, onCall) {
    onCall = onCall || function() {
        };
    Object.defineProperty(Assertion.prototype, name, {
        get: function() {
            onCall();
            return this;
        },
        enumerable: true
    });
};

addChain方法添加屬性到原型鏈中,該屬性在調用方法後返回調用者本身。該方法在should.js的鏈式調用中起着重要的作用。

同時,Assertion類還支持別名功能,alias方法使用Object對象的getOwnPropertyDescriptor方法來對屬性是否存在進行判斷,並調用defineProperty進行賦值。

Assertion類在原型鏈中定義了assert方法,用來對各級限制條件進行判斷。assert方法與普通方法不同,它並未採用參數來進行一些參數的傳遞,而是通過assert方法所在的Assertion對象的params屬性來進行參數的傳遞。因爲在Assertion對象中存儲了相關的信息,使用這個方法來進行參數傳遞方便在各級中assert函數的調用方便。具體使用方法我們將在擴展的分析時提到。

assert: function(expr) {
    if(expr) return this;

    var params = this.params;

    if('obj' in params && !('actual' in params)) {
        params.actual = params.obj;
    } else if(!('obj' in params) && !('actual' in params)) {
        params.actual = this.obj;
    }

    params.stackStartFunction = params.stackStartFunction || this.assert;
    params.negate = this.negate;

    params.assertion = this;

    throw new AssertionError(params);
}

Assertion類也定義了一個fail方法能夠讓用戶直接調用從而拋出一個Assertion的Error。

fail: function() {
    return this.assert(false);
}

assertion-error.js

在此文件中,定義了assertion中拋出來的錯誤,同時在其中定義了一些信息存儲的函數例如messagedetail等,能夠讓錯誤在被捕獲的時候帶上一些特定的信息從而方便進行判斷與處理。由於實現較爲簡單,因此在此就不貼出代碼,需要了解的人可以自己去查閱should.js的源碼。

ext/bool.js

下面簡單介紹一個Assertion的擴展的工作方式。讓我們能夠對should.js的工作原理有一個更加深刻的理解。

module.exports = function(should, Assertion) {
    Assertion.add('true', function() {
        this.is.exactly(true);
    });
    
    Assertion.alias('true', 'True');

    Assertion.add('false', function() {
        this.is.exactly(false);
    });
    Assertion.alias('false', 'False');

    Assertion.add('ok', function() {
        this.params = {operator: 'to be truthy'};

        this.assert(this.obj);
    });
};

//should.js
should.use = function (f) {
    f(should, should.Assertion);
    return this;
};

//use
'1'should.be.true();

通過上面的擴展模塊代碼以及should.js文件中的use函數,我們可以發現,use函數向擴展模塊傳入了should方法和Assertion構造函數。在bool.js這個擴展模塊中,它通過調用Assertion對象上的add函數來添加新的判斷方式,並且通過params參數來告訴Assertion對象如果判斷失敗應該如何提示用戶。

感想

should.js如何實現鏈式調用?

Assertion類中,有一個addChain方法,該方法爲某些屬性定義了一些在getter函數中調用的操作方法,並且返回對象本身。通過這個方法,在ext/chain.js中,它爲should.js中常見的語義詞添加了屬性,並通過返回對象本身來達到鏈式調用的Assertion對象傳遞。

['an', 'of', 'a', 'and', 'be', 'has', 'have', 'with', 'is', 'which', 'the', 'it'].forEach(function(name) {
    Assertion.addChain(name);
});

以下兩段代碼在結果上是一模一樣的效果:

'1'.shoud.be.a.Number();
'1'.should.be.be.be.be.a.a.a.a.Number();

should.js的實現方式有哪些值得借鑑的地方?

  1. should.js中,通過將一些語義詞添加爲屬性值並返回Assertion對象本身,因此有效解決了鏈式調用的問題。
  2. 通過Asseriton對象的屬性來進行參數的傳遞,而不是通過函數參數,從而有效避免了函數調用時參數的傳遞問題以及多層調用時結構的複雜。
  3. should.js通過擴展的方式來添加其判斷的函數,保證了良好的擴展性,避免了代碼耦合在一起,通過也爲其他人編寫更多的擴展代碼提供了接口。
  4. should.js通過extend方法,讓should(obj)obj.should兩種方式達到了相同的效果。通過在defineProperty中定義should屬性並且在回調函數中用should(obj)的方式來獲取obj對象。
  5. 通過拋出錯誤而不是返回布爾值的方式來通知用戶,能夠更加明顯的通知用戶,也方便向上拋出異常進行傳遞。

總結

總的來說,should.js是一個比較小而精的測試框架,他能夠滿足在開發過程中所需要的大部分測試場景,同時也支持自己編寫擴展來強化它的功能。在設計上,這個框架使用了不少巧妙的方法,避免了一些複雜的鏈式調用與參數傳遞等問題,而且結構清晰,比較適合進行閱讀與學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章