构建辅助函数
本教程假设您熟悉插件 核心概念。如果您还没有阅读过这篇文章,建议您在继续之前阅读。
提供可链式辅助断言是 Chai 公开插件工具的最常见用途。在我们进入基础知识之前,我们需要一个主题,我们将扩展 Chai 的断言来理解它。为此,我们将使用一个非常小的数据模型对象。
/**
* # Model
*
* A constructor for a simple data model
* object. Has a `type` and contains arbitrary
* attributes.
*
* @param {String} type
*/
function Model (type) {
this._type = type;
this._attrs = {};
}
/**
* .set (key, value)
*
* Set an attribute to be stored in this model.
*
* @param {String} key
* @param {Mixted} value
*/
Model.prototype.set = function (key, value) {
this._attrs[key] = value;
};
/**
* .get (key)
*
* Get an attribute that is stored in this model.
*
* @param {String} key
*/
Model.prototype.get = function (key) {
return this._attrs[key];
};
实际上,这可以是任何从 node 中的 ORM 数据库返回的数据模型对象,或者是在浏览器中从您选择的 MVC 框架构造的。
希望我们的 Model
类不言自明,但作为示例,这里我们构造了一个人对象。
var arthur = new Model('person');
arthur.set('name', 'Arthur Dent');
arthur.set('occupation', 'traveller');
console.log(arthur.get('name')); // Arthur Dent
现在我们有了主题,我们可以继续学习插件的基础知识。
添加语言链
现在我们开始进入有趣的部分了!添加属性和方法是 Chai 的插件 API 真正的用途。
添加属性
本质上,定义属性可以使用 Object.defineProperty
,但我们建议您使用 Chai 的工具助手来确保整个实现的标准化。
在本例中,我们希望以下测试用例通过:
var arthur = new Model('person');
expect(arthur).to.be.a.model;
为此,我们将使用 addProperty
工具。
utils.addProperty(Assertion.prototype, 'model', function () {
this.assert(
this._obj instanceof Model
, 'expected #{this} to be a Model'
, 'expected #{this} to not be a Model'
);
});
简洁明了。Chai 可以从这里开始。还值得一提的是,由于这种扩展模式经常被使用,Chai 使其变得更加容易。以下内容可以代替上面的第一行:
Assertion.addProperty('model', function () { // ...
所有链式扩展工具都作为 utils
对象的一部分以及直接在断言构造函数上提供。但是,在本文件的其余部分中,我们将直接从 Assertion
调用方法。
添加方法
注意:多个插件使用
addMethod
定义相同的方法名会导致冲突,最后一个注册的插件将获胜。插件 API 有待在 Chai 的未来版本中进行重大改进,其中包括处理此冲突。在此期间,请优先使用overwriteMethod
。
虽然属性是一种优雅的解决方案,但它可能不足以满足我们正在构建的辅助函数。由于我们的模型具有类型,因此断言我们的模型属于特定类型将是有益的。为此,我们需要一个方法。
// goal
expect(arthur).to.be.a.model('person');
// language chain method
Assertion.addMethod('model', function (type) {
var obj = this._obj;
// first, our instanceof check, shortcut
new Assertion(this._obj).to.be.instanceof(Model);
// second, our type check
this.assert(
obj._type === type
, "expected #{this} to be of type #{exp} but got #{act}"
, "expected #{this} to not be of type #{act}"
, type // expected
, obj._type // actual
);
});
所有对 assert
的调用都是同步的,因此如果第一个调用失败,则将抛出 AssertionError
,并且第二个调用将不会执行。解释消息并处理任何失败断言的显示取决于测试运行器。
方法作为属性
Chai 包含一个独特的工具,允许您构建一个可以充当属性或方法的语言链。我们称之为“可链式方法”。尽管我们展示了“是模型的模型”既是属性又是方法,但这些断言不适合使用可链式方法。
何时使用
为了理解何时最好使用可链式方法,我们将检查 Chai 核心中的一个可链式方法。
var arr = [ 1, 2, 3 ]
, obj = { a: 1, b: 2 };
expect(arr).to.contain(2);
expect(obj).to.contain.key('a');
为了使这工作,需要两个单独的函数。一个是当链用作属性或方法时调用的函数,另一个是仅用作方法时调用的函数。
在这些示例中,以及在核心中的所有其他可链式方法中,contain
作为属性的唯一功能是将 contains
标记设置为 true。这指示 keys
以不同的方式行事。在这种情况下,当 key
与 contain
结合使用时,它将检查键的包含,而不是检查对所有键的完全匹配。
何时不使用
假设我们为 model
设置了一个可链式方法,使其按照我们上面指示的方式行事:如果用作属性,则执行 instanceof
检查,如果用作方法,则执行 _type
检查。将发生以下冲突…
以下将起作用…
expect(arthur).to.be.a.model;
expect(arthur).to.be.a.model('person');
expect(arr).to.not.be.a.model;
但以下将不起作用…
expect(arthur).to.not.be.a.model('person');
请记住,由于用作属性断言的函数在也用作方法时被调用,并且否定影响设置后的所有断言,因此我们将收到类似于 expected [object Model] not to be instance of [object Model]
的错误消息。因此,在构建可链式方法时,请遵循此一般准则。
在构建可链式方法时,属性函数应该只用于为以后修改现有断言的行为设置标记。
合适的示例
为了与我们的模型示例一起使用,我们将构建一个示例,该示例允许我们准确地测试亚瑟的年龄,或者链接到 Chai 的数字比较器,例如 above
、below
和 within
。您需要学习如何在不破坏核心功能的情况下覆盖方法,但我们稍后会讲到。
我们的目标将允许所有以下操作通过。
expect(arthur).to.have.age(27);
expect(arthur).to.have.age.above(17);
expect(arthur).to.not.have.age.below(18);
首先,让我们开始编写用于可链式方法的两个函数。首先是调用 age
方法时要使用的函数。
function assertModelAge (n) {
// make sure we are working with a model
new Assertion(this._obj).to.be.instanceof(Model);
// make sure we have an age and its a number
var age = this._obj.get('age');
new Assertion(age).to.be.a('number');
// do our comparison
this.assert(
age === n
, "expected #{this} to have age #{exp} but got #{act}"
, "expected #{this} to not have age #{act}"
, n
, age
);
}
到目前为止,这应该不言自明。现在是我们的属性函数。
function chainModelAge () {
utils.flag(this, 'model.age', true);
}
稍后,我们将教我们的数字比较器查找该标记并更改其行为。由于我们不想破坏核心方法,因此我们需要安全地覆盖该方法,但我们稍后会讲到。让我们先完成这里…
Assertion.addChainableMethod('age', assertModelAge, chainModelAge);
完成。现在我们可以断言亚瑟的确切年龄。在学习如何覆盖方法时,我们将再次使用此示例。
覆盖语言链
现在我们可以成功地向语言链添加断言,我们应该努力能够安全地覆盖现有断言,例如来自 Chai 核心或其他插件的断言。
Chai 提供了许多工具,允许您覆盖已存在断言的现有行为,但如果断言的主题不满足您的标准,则恢复到已定义的断言行为。
让我们从覆盖属性的简单示例开始。
覆盖属性
在本例中,我们将覆盖 Chai 核心提供的 ok
属性。默认行为是,如果对象为真值,则 ok
将通过。我们想要改变这种行为,以便当 ok
与模型实例一起使用时,它验证模型是否格式良好。在我们的示例中,如果模型具有 id
属性,我们将认为模型 ok
。
让我们从基本的覆盖工具和一个基本的断言开始。
chai.overwriteProperty('ok', function (_super) {
return function checkModel () {
var obj = this._obj;
if (obj && obj instanceof Model) {
new Assertion(obj).to.have.deep.property('_attrs.id').a('number');
} else {
_super.call(this);
}
};
});
覆盖结构
如您所见,覆盖的主要区别在于第一个函数只传递一个 _super
参数。这是最初存在的函数,您应该确保在您的标准不匹配的情况下调用它。其次,您会注意到我们立即返回一个新函数,它将用作实际的断言。
有了这个,我们就可以编写正断言了。
var arthur = new Model('person');
arthur.set('id', 42);
expect(arthur).to.be.ok;
expect(true).to.be.ok;
上述预期将会通过。在使用模型时,它将运行我们自定义的断言,而在使用非模型时,它将恢复到原始行为。但是,如果我们尝试否定模型上的 ok
断言,我们将遇到一些麻烦。
var arthur = new Model('person');
arthur.set('id', 'dont panic');
expect(arthur).to.not.be.ok;
我们期望此预期也能够通过,因为我们的语句被否定了,并且 id 不是一个数字。不幸的是,否定标志没有传递给我们的数字断言,因此它仍然期望值是一个数字。
传递标志
为此,我们将通过将所有标志从原始断言转移到新断言来扩展此断言。最终的属性覆盖将如下所示。
chai.overwriteProperty('ok', function (_super) {
return function checkModel () {
var obj = this._obj;
if (obj && obj instanceof Model) {
new Assertion(obj).to.have.deep.property('_attrs.id'); // we always want this
var assertId = new Assertion(obj._attrs.id);
utils.transferFlags(this, assertId, false); // false means don't transfer `object` flag
assertId.is.a('number');
} else {
_super.call(this);
}
};
});
现在,否定标志包含在您的新断言中,我们可以成功地处理 id 类型上的正断言和负断言。我们将属性断言保留为原样,因为我们希望它始终在 id 不存在时失败。
增强错误消息
不过,我们还需要进行一个小的修改。如果我们的断言由于 id 属性类型错误而失败,我们将收到一个错误消息,指出 expected 'dont panic' to [not] be a number
。在运行大型测试套件时,这并不完全有用,因此我们将提供更多信息。
var assertId = new Assertion(obj._attrs.id, 'model assert ok id type');
这将更改我们的错误消息,使其更具信息性,即 model assert ok id type: expected 'dont panic' to [not] be a number
。信息量大得多!
覆盖方法
覆盖方法遵循覆盖属性的相同结构。在此示例中,我们将回到断言亚瑟的年龄是否高于最小阈值的示例。
var arthur = new Model('person');
arthur.set('age', 27);
expect(arthur).to.have.age.above(17);
我们已经有了 age
链,它使用 model.age
来标记断言,因此我们只需要检查它是否存在。
Assertion.overwriteMethod('above', function (_super) {
return function assertAge (n) {
if (utils.flag(this, 'model.age')) {
var obj = this._obj;
// first we assert we are actually working with a model
new Assertion(obj).instanceof(Model);
// next, make sure we have an age
new Assertion(obj).to.have.deep.property('_attrs.age').a('number');
// now we compare
var age = obj.get('age');
this.assert(
age > n
, "expected #{this} to have an age above #{exp} but got #{act}"
, "expected #{this} to not have an age above #{exp} but got #{act}"
, n
, age
);
} else {
_super.apply(this, arguments);
}
};
});
这涵盖了正面和负面情况。在这种情况下,无需传递标志,因为 this.assert
会自动处理。相同的模式也可以用于 below
和 within
。