一、前言
断言库是单元测试的重要组成部分,在编写单元测试代码时,通过断言库来描述代码逻辑的预期效果,从而验证代码逻辑的正确性。
断言库一般分为两大类: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 类解耦合。并且开发者可以根据自己的使用场景定制断言方法,从而解决包大小的问题。
九、写在最后
以上就是本文的全部内容,希望能够给你带来帮助,欢迎「关注」、「点赞」、「转发」。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!