should.js源码分析与学习

Posted 本期节目

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

 
   
   
 
  1. var should = function should(obj) {

  2.    return (new should.Assertion(obj));

  3. };

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

  5. should.Assertion = require('./assertion');

  6. should.format = util.format;

  7. should.type = require('should-type');

  8. should.util = util;

  9. should.config = require('./config');

  10. exports = module.exports = should;

should.js入口文件初始化了一个类,并将所有文件中其他的模块进行引入。同时将自己export出去,让自己能够被require到。

 
   
   
 
  1. should.extend = function (propertyName, proto) {

  2.    propertyName = propertyName || 'should';

  3.    proto = proto || Object.prototype;

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

  5. Object.defineProperty(proto, propertyName, {

  6.    set: function () {

  7.    },

  8.    get: function () {

  9.        return should(util.isWrapperType(this) ? this.valueOf() : this);

  10.    },

  11.    configurable: true

  12. });

  13. return {

  14.    name: propertyName, descriptor: prevDescriptor, proto: proto};

  15. };

should.js自身定义了一个extend方法,用于兼容should.js的另一种调用方式,即 should(obj)的方式等于 should.js的常规调用方式 obj.should,从而兼容另一种写法。

 
   
   
 
  1. should

  2.    .use(require('./ext/assert'))

  3.    .use(require('./ext/chain'))

  4.    .use(require('./ext/bool'))

  5.    .use(require('./ext/number'))

  6.    .use(require('./ext/eql'))

  7.    .use(require('./ext/type'))

  8.    .use(require('./ext/string'))

  9.    .use(require('./ext/property'))

  10.    .use(require('./ext/error'))

  11.    .use(require('./ext/match'))

  12.    .use(require('./ext/contain'));

should.js中还定义了use方法,从而让我们能够自己编写一些类型判断例如isNumber等函数导入到项目中,从而方便进行测试。项目目录中的 ext文件夹就是编写的一些简单的should.js的扩展。后面将在介绍扩展时对两者的工作原理以及使用方法进行介绍。

assertion.js

 
   
   
 
  1. function Assertion(obj) {

  2.    this.obj = obj;

  3.    /**

  4.     * any标志位

  5.     * @type {boolean}

  6.     */

  7.    this.anyOne = false;

  8.    /**

  9.     * not标志位

  10.     * @type {boolean}

  11.     */

  12.    this.negate = false;

  13.    this.params = {actual: obj};

  14. }

assertion.js中定义了一个Assertion类,其中any为should.js中的 any方法的标志位,而not则为其 not方法的标志位。

 
   
   
 
  1. Assertion.add = function(name, func) {

  2.    var prop = {enumerable: true, configurable: true};

  3.    prop.value = function() {

  4.        var context = new Assertion(this.obj, this, name);

  5.        context.anyOne = this.anyOne;

  6.        try {

  7.            func.apply(context, arguments);

  8.        } catch(e) {

  9.            //check for fail

  10.            if(e instanceof AssertionError) {

  11.                //negative fail

  12.                if(this.negate) {

  13.                    this.obj = context.obj;

  14.                    this.negate = false;

  15.                    return this;

  16.                }

  17.                if(context !== e.assertion) {

  18.                    context.params.previous = e;

  19.                }

  20.                //positive fail

  21.                context.negate = false;

  22.                context.fail();

  23.            }

  24.            // throw if it is another exception

  25.            throw e;

  26.        }

  27.        //negative pass

  28.        if(this.negate) {

  29.            context.negate = true;//because .fail will set negate

  30.            context.params.details = 'false negative fail';

  31.            context.fail();

  32.        }

  33.        //positive pass

  34.        if(!this.params.operator) this.params = context.params;//shortcut

  35.        this.obj = context.obj;

  36.        this.negate = false;

  37.        return this;

  38.    };

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

  40. };

assertion.js中的add方法在Assertion的原型链中添加自定义命名的方法,从而让我们能够打包一些判断的方法来进行调用,不需要重复进行代码的编写。该方法具体的使用方式我们在后面对扩展进行讲解时将会提到。

 
   
   
 
  1. Assertion.addChain = function(name, onCall) {

  2.    onCall = onCall || function() {

  3.        };

  4.    Object.defineProperty(Assertion.prototype, name, {

  5.        get: function() {

  6.            onCall();

  7.            return this;

  8.        },

  9.        enumerable: true

  10.    });

  11. };

addChain方法添加属性到原型链中,该属性在调用方法后返回调用者本身。该方法在 should.js的链式调用中起着重要的作用。

同时,Assertion类还支持别名功能, alias方法使用Object对象的 getOwnPropertyDescriptor方法来对属性是否存在进行判断,并调用 defineProperty进行赋值。

Assertion类在原型链中定义了 assert方法,用来对各级限制条件进行判断。 assert方法与普通方法不同,它并未采用参数来进行一些参数的传递,而是通过 assert方法所在的 Assertion对象的 params属性来进行参数的传递。因为在 Assertion对象中存储了相关的信息,使用这个方法来进行参数传递方便在各级中 assert函数的调用方便。具体使用方法我们将在扩展的分析时提到。

 
   
   
 
  1. assert: function(expr) {

  2.    if(expr) return this;

  3.    var params = this.params;

  4.    if('obj' in params && !('actual' in params)) {

  5.        params.actual = params.obj;

  6.    } else if(!('obj' in params) && !('actual' in params)) {

  7.        params.actual = this.obj;

  8.    }

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

  10.    params.negate = this.negate;

  11.    params.assertion = this;

  12.    throw new AssertionError(params);

  13. }

Assertion类也定义了一个 fail方法能够让用户直接调用从而抛出一个Assertion的Error。

 
   
   
 
  1. fail: function() {

  2.    return this.assert(false);

  3. }

assertion-error.js

在此文件中,定义了assertion中抛出来的错误,同时在其中定义了一些信息存储的函数例如 messagedetail等,能够让错误在被捕获的时候带上一些特定的信息从而方便进行判断与处理。由于实现较为简单,因此在此就不贴出代码,需要了解的人可以自己去查阅should.js的源码。

ext/bool.js

下面简单介绍一个 Assertion的扩展的工作方式。让我们能够对should.js的工作原理有一个更加深刻的理解。

 
   
   
 
  1. module.exports = function(should, Assertion) {

  2.    /**

  3.     * 判断是否为true

  4.     */

  5.    Assertion.add('true', function() {

  6.        this.is.exactly(true);

  7.    });

  8.    /**

  9.     * 别名为True

  10.     */

  11.    Assertion.alias('true', 'True');

  12.    /**

  13.     * 判断是否为false

  14.     */

  15.    Assertion.add('false', function() {

  16.        this.is.exactly(false);

  17.    });

  18.    /**

  19.     * 别名False

  20.     */

  21.    Assertion.alias('false', 'False');

  22.    /**

  23.     * 通过对象检查来判断对象是否为空

  24.     */

  25.    Assertion.add('ok', function() {

  26.        this.params = {operator: 'to be truthy'};

  27.        this.assert(this.obj);

  28.    });

  29. };

  30. //should.js

  31. should.use = function (f) {

  32.    f(should, should.Assertion);

  33.    return this;

  34. };

  35. //use

  36. '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对象传递。

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

  2.    Assertion.addChain(name);

  3. });

以下两段代码在结果上是一模一样的效果:
'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是一个比较小而精的测试框架,他能够满足在开发过程中所需要的大部分测试场景,同时也支持自己编写扩展来强化它的功能。在设计上,这个框架使用了不少巧妙的方法,避免了一些复杂的链式调用与参数传递等问题,而且结构清晰,比较适合进行阅读与学习。


以上是关于should.js源码分析与学习的主要内容,如果未能解决你的问题,请参考以下文章

Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段

Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段

C语言100个经典算法源码片段

Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段

Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段

Android内核源码bionic目录下的子目录arch-arm源码分析笔记