为了研究与学习某些测试框架的工做原理,同时也为了完成培训中实现一个简单的测试框架的缘由,我对should.js的代码进行了学习与分析,如今与你们来进行交流下。javascript
以前是放在segmentfault上面的,如今往掘金上面也同步一份。java
其中ext
为文件夹,其他为js文件。segmentfault
其中should.js
为整个项目入口,asssertion.js
为should.js中的类,负责对测试信息进行记录。assertion-error.js
为should.js定义了一个错误类,负责存储错误信息。config.js
中存储了一些should.js中的一些配置信息。util.js
中则定义了一些项目中经常使用的工具函数。bash
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到。app
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中抛出来的错误,同时在其中定义了一些信息存储的函数例如message
和detail
等,可以让错误在被捕获的时候带上一些特定的信息从而方便进行判断与处理。因为实现较为简单,所以在此就不贴出代码,须要了解的人能够本身去查阅should.js的源码。
ext/bool.js
下面简单介绍一个Assertion
的扩展的工做方式。让咱们可以对should.js的工做原理有一个更加深入的理解。
module.exports = function(should, Assertion) {
/** * 判断是否为true */
Assertion.add('true', function() {
this.is.exactly(true);
});
/** * 别名为True */
Assertion.alias('true', 'True');
/** * 判断是否为false */
Assertion.add('false', function() {
this.is.exactly(false);
});
/** * 别名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
的实现方式有哪些值得借鉴的地方?should.js
中,经过将一些语义词添加为属性值并返回Assertion
对象自己,所以有效解决了链式调用的问题。Asseriton
对象的属性来进行参数的传递,而不是经过函数参数,从而有效避免了函数调用时参数的传递问题以及多层调用时结构的复杂。should.js
经过扩展的方式来添加其判断的函数,保证了良好的扩展性,避免了代码耦合在一块儿,经过也为其余人编写更多的扩展代码提供了接口。should.js
经过extend方法,让should(obj)
与obj.should
两种方式达到了相同的效果。经过在defineProperty
中定义should属性而且在回调函数中用should(obj)
的方式来获取obj
对象。总的来讲,should.js
是一个比较小而精的测试框架,他可以知足在开发过程当中所须要的大部分测试场景,同时也支持本身编写扩展来强化它的功能。在设计上,这个框架使用了很多巧妙的方法,避免了一些复杂的链式调用与参数传递等问题,并且结构清晰,比较适合进行阅读与学习。