剖析断言库 should.js 实现原理

一、前言

  断言库是单元测试的重要组成部分,在编写单元测试代码时,通过断言库来描述代码逻辑的预期效果,从而验证代码逻辑的正确性。

  断言库一般分为两大类:TDD(Test Driven Development)和 BDD(Behavior Driven Development)

  • TDD: assert.js

  • BDD: should.js 和 expect.js

  BDD 与 TDD 的区别主要在于:TDD 解决了代码级别的验证,但是测试代码与需求的符合问题解决的不是很好,而 BDD 则是让除了开发人员之外更多的角色参与到用例的评审,确保需求与测试代码相匹配。

  下面是 TDD 风格断言库 assert.js 的示例代码:

  assert.typeOf(foo, 'string');
  assert.equal(foo, 'bar');
  assert.lengthOf(foo, 3)
  assert.property(tea, 'flavors');
  assert.lengthOf(tea.flavors, 3);

  再看 BDD 风格断言库 should.js 的示例代码:

  foo.should.be.a('string');
  foo.should.equal('bar');
  foo.should.have.lengthOf(3);
  tea.should.have.property('flavors').with.lengthOf(3);

  相比较下,可读性更强,更容易让非开发人员理解断言的意思。

  接下来,本文就带大家一起探索 should.js 的实现原理。

二、扩展对象属性

const num = 10;
num.should.be.above(8);

  当我们使用 should.js 时,他会为所有对象挂载一个名为 should 的属性。这个主要是依靠 JavaScript 中属性的查找机制,利用 Object.defineProperty 方法在对象的原型对象上进行扩展:

class Assertion {
  // ...
};

const should = (obj) => {
  return new Assertion(obj);
};

Object.defineProperty(Object.prototype, 'should', {
  set() {},
  get() {
    return should(this);
  },
  configurable: true
})

  Object.defineProperty 方法需要手动做健壮性处理,当属性没有被成功定义时,会抛出一个 TypeError。相比较下,使用 ES6 提供的元编程静态方法 Reflect.defineProperty 可以解决这一问题:

const ans1 = Reflect.defineProperty(Object.prototype, 'should', {
  set() {},
  get() {
    return should(this);
  },
  configurable: false,
})

const ans2 = Reflect.defineProperty(Object.prototype, 'should', {
  set() {},
  get() {
    return should(this);
  },
})

console.log(ans1, ans2); // true false

  上述示例中第一次设置 should 时将 configurable 属性设置为 false,再次设置 should 属性的描述符时,不会抛出 TypeError,而是通过 false 返回值告诉开发者此次操作失败。

三、添加链式调用

  链式调用的实现方式主要是在方法中将当前实例返回:

const should = (obj) => {
  return new Assertion(obj);
};

  这样就可以继续调用当前实例上的属性或者方法。

四、断言方法机制

class Assertion {
  constructor(obj) {
    this.obj = obj;
  }

  above (n) {
    return this.assert(this.obj > n);
  }

  assert (expr) {
    if (expr) {
      return console.log('success');
    }
    return console.log('fail');
  }
};

  前文通过 Reflect.defineProperty 方法扩展 should 属性时,返回 Assertion 断言类的实例,该实例上挂载诸如 above 这样的断言方法,但是他们内部都是通过 assert 方法来判断表达式返回值的真假来实现断言功能。

五、优化断言失败提示

const { AssertionError } = require('assert');

class Assertion {
  constructor(obj) {
    this.obj = obj;
  }

  above (n, message) {
    this.params = { operator: 'should be above ' + n, message };
    return this.assert(this.obj > n);
  }

  assert (expr) {
    if (expr) {
      return console.log('success');
    }
    let { message } = this.params;
    if (!message) {
      message = `expected ${this.obj} ${this.params.operator}`
    }

    const err = new AssertionError({
      message,
      actual: this.obj,
      stackStartFn: this.assert,
    })

    throw err;
  }
};

  这里利用 Node.js 提供的 AssertionError 类来显示断言失败的函数调用堆栈和友好信息。在开发者未手动传入 message 的情况下,可以根据对外提供的方法构造一个兜底文案,提高开发体验。

六、结构助词

num.should.be.above(8);

  类似 be 这样的结构助词,不影响断言逻辑的判断,只是用来增强断言的可读性。

  同样可以采用 Reflect.defineProperty 方法来实现,但是需要注意 should 属性返回的是 Assertion 的实例,所以这些属性应该扩展在 Assertion 的原型属性上。

['an', 'of', 'a', 'and', 'be', 'have', 'with', 'is', 'which', 'the'].forEach(name => {
  Reflect.defineProperty(Assertion.prototype, name, {
    get () {
      return this;
    }
  })
})

七、否定属性

class Assertion {
  constructor(obj) {
    this.obj = obj;
  }

  assert (expr) {
    let actualExpr = this.negate ? !expr : expr;

    // 非关键代码省略
  }

  get not () {
    this.negate = !this.negate
    return this;
  }
}

  示例上添加 negate 属性来标记当前的状态。这里特别需要注意 not 属性中为什么不使用 this.negate = false,主要是考虑类似双重否定的情况:

const num = 20;
num.should.not.not.be.above(10);

八、插件系统

  随着应用场景越来越复杂,我们会在 Assertion 类上添加越来越多的方法,这种方式存在以下两个缺陷:

  • 对 Assertion 类侵入性很强,增加了额外的维护成本。

  • 包大小问题

// 插件注册方法
should.use = function(plugin) {
  plugin(Assertion);
  return this;
}

Assertion.add = function(name, fn) {
  Object.defineProperty(Assertion.prototype, name, {
    value () {
      fn.apply(this, arguments);
    }
  })
}

function numberPlugin(Assertion) {
  Assertion.add('above', function (n, message) {
    this.params = { operator: 'to be above ' + n, message };
    return this.assert(this.obj > n);
  });

  Assertion.add('below', function(n, message) {
    this.params = { operator: 'to be below ' + n, message };
    return this.assert(this.obj < n);
  });
}
// 注册插件
should.use(numberPlugin);

  通过插件机制,将断言方法与 Assertion 类解耦合。并且开发者可以根据自己的使用场景定制断言方法,从而解决包大小的问题。

九、写在最后

  以上就是本文的全部内容,希望能够给你带来帮助,欢迎「关注」「点赞」「转发」

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值