Создание помощника
В этом руководстве предполагается, что вы знакомы с основными концепциями плагина core concepts. Если вы еще не читали эту статью, рекомендуется сделать это перед продолжением.
Наиболее распространенное использование утилит плагина 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];
};
Практически это может быть любой объект модели данных, возвращаемый базой данных ORM в Node или созданный из вашей MVC-рамки по выбору в браузере.
Надеемся, что наш Model класс понятен сам по себе, но в качестве примера здесь мы создаем объект человека.
var arthur = new Model('person');
arthur.set('name', 'Arthur Dent');
arthur.set('occupation', 'traveller');
console.log(arthur.get('name')); // Arthur Dent
Теперь, когда у нас есть предмет, мы можем перейти к основам плагинов.
Добавление цепочек языка
Теперь мы переходим к интересному моменту! Добавление свойств и методов — это то, для чего предназначен API плагинов Chai.
Добавление свойств
По сути, определение свойства можно выполнить с помощью Object.defineProperty, но мы рекомендуем использовать утилиты помощников Chai, чтобы обеспечить стандартную реализацию в целом.
Для этого примера мы хотим, чтобы следующее тестовое case прошло:
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. Однако в остальной части этого документа мы будем вызывать методы непосредственно из 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 have #{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);
Просмотр API addChainableMethod
Готово. Теперь мы можем утверждать точный возраст Артура. Мы продолжим этот пример позже, когда узнаем, как перезаписывать методы.
Перезапись цепочек языка
Теперь, когда мы можем успешно добавлять утверждения в цепочку языка, мы должны научиться безопасно перезаписывать существующие утверждения, такие как те, что из ядра Chai или других плагинов.
Chai предоставляет ряд утилит, которые позволяют перезаписывать существующее поведение уже существующего утверждения, но возвращаться к уже определенному поведению утверждения, если предмет утверждения не соответствует вашим критериям.
Начнем с простого примера перезаписи свойства.
Перезапись свойств
В этом примере мы будем перезаписывать свойство ok, предоставляемое ядром Chai. По умолчанию ok проходит, если объект является истинным. Мы хотим изменить это поведение, чтобы при использовании ok с экземпляром модели она проверяла, что модель сформирована правильно. В нашем примере мы будем считать модель 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').a('number');
} else {
_super.call(this);
}
};
});
Просмотр API overwriteProperty
Структура перезаписи
Как вы можете видеть, основное различие в перезаписи состоит в том, что первая функция принимает только один аргумент _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.
© 2017 Chai.js Assertion Library
Licensed under the MIT License.
https://www.chaijs.com/guide/helpers/