Сегодня темой нашей беседы станет Unit-тестирование в JavaScript, а точнее — один из его аспектов, т.к. Unit-тестирование — тема очень большая, чтоб рассказать о ней в одной статье.
Графические интерфейсы современных web-приложения с каждым годом становятся значительно сложнее. На них возлагается больше логики, сложное поведение элементов, работа с данными. И все эти действия во избежание постоянных регрессионных ошибок нужно проверять автоматизированными тестами. К сожалению, многие жертвуют качеством продукта, на каком-то этапе выигрывая во времени, но наступает такая критическая точка, когда стоимость какого-либо изменения становится гораздо дороже той бизнес-ценности, которую получит клиент. У ручных тестеров с каждым днём прибавляется количество проверок, которые им необходимо сделать, т.к. нужно проверять не только разрабатываемый функционал, так и уже разработанный.
В такой ситуации необходимо брать на вооружение Unit-тестирование — самый нижний уровень тестирования, проверяющий модули по отдельности и позволяющий сразу локализовать источник ошибок. Да, в клиентском коде также может быть много логики, и его обязательно нужно проверять. И тут на помощь нам приходит BDD-фрэймворк для JavaScript — Jasmine, который позволяет писать очень лаконичные, хорошо читаемые и понятные тесты.
Но это небольшое лирическое отступление. Перейдём ближе к теме статьи.
Зачастую в проекте возникает необходимость в некой нетривиальной проверке, которая инкапсулирует в себе всю логику и может использоваться в любом наборе тестов проекта. Сейчас мы разберёмся, как создавать Jasmine-совместимые дополнительные matcher’ы. В терминологии документации Jasmine они называются «custom matcher», на русский мне это удалось перевести как «необычный совпадатель» =) В связи с этим «красивым» переводом предлагаю в рамках статьи называть их дополнительными matcher’ами.
Итак, дополнительный matcher по своей сути — это функция проверки, которая на вход принимает реальное и ожидаемое значение (те самые actual
и expected
, которые так часто всплывают красными буквами, если тест валится). Эта фабрика передаётся в Jasmine (лучше всего это делать из вызова beforeEach
), после чего наш новый matcher становится доступным для всех тестов (it
) внутри данного вызова describe
. Между тестами дополнительные matcher’ы недоступны. Название свойства фабрики будет являться именем matcher’а, доступного в возвращаемом значении вызова функции expect
.
В качестве примера, код которого опубликован в конце этой статьи, мы сделаем дополнительный matcher toBeFasterThen
, сравнивающий скорость передаваемого в неё объекта-существа со скоростью второго объекта. Конечно, пример у нас получился исключительно вымышленным и особой ценности не несёт, но, опираясь на данный подход, можно делать сложные проверки с простым вызовом matcher’а снаружи.
Фабрики matcher’ов
Фабрика дополнительного matcher’а принимает на вход два параметра: util
, в котором находится набор вспомогательных функций (такие как equals
, contains
, buildFailureMessage
), и customEqualityTesters
— массив функций для специфичной проверки на равенство объектов, который должен передаваться, если вызывается util.equals
. Эти параметры (util
и customEqualityTesters
) доступны для использования внутри вызовов создаваемого matcher’а.
Фабрика должна вернуть объект со свойством compare
, в котором будет функция сравнения ожиданий с действительностью.
Функция compare
Функция сравнения получает в качестве первого аргумента реально полученное значение (actual
), передаваемое в expect()
, и значение (если такое определено), передаваемое самому matcher’у в качестве второго аргумента.
toBeFasterThen
на вход принимает (в качестве expected
) обязательный аргумент — объект-существо со свойством speed. Если ваш аргумент необязательный, не забудьте установить значение по умолчанию, если аргумент отсутствует.
Возвращаемый результат
Функция проверки в качестве результата должна возвращать объект со свойством pass
, являющимся булевым значением успешности проверки. Свойство pass
показывает, удовлетворило ожидаемое значение реальному (true
) или нет (false
). Если ожидание (expect
) вызвано в цепочке с отрицанием .not
, оно будет проверять значение, противоположное полученному, чтобы осуществить проверку.
Сообщения об ошибках
Если сообщение об ошибке не определено, ожидание будет пытаться сформировать ответ matcher’a своими силами, а именно — оно разобьет название matcher’a на слова и сформирует при правильном имени matcher’a вполне хорошее предложение — очень красивое решение, на мой взгляд. Однако, если в возвращаемом значении имеется свойство message
, оно будет использовано для описания неудачных проверок.
Если проверка успешна, то соответствующее сообщение об ошибке будет использоваться в случае отрицательного ожидания — когда ожидание использует .not
. В примере будет такой тест.
Если проверка провалена, то задаваемое сообщение об ошибке также может задаваться и для положительного результата ожидания.
В конце функции проверки возвращаем получившийся объект.
Дополнительное негативное сравнение
Если вам нужна какая-то специфичная логика для негативного сравнения (при .not
), и обычная булева инверсия значения compare
вам не подходит, вы можете в фабрику matcher’а рядом со свойством compare
добавить ещё одно свойство negativeCompare
, в котором нужно разместить функцию, вызываемую при использовании .not
в ожидании. Это свойство — необязательное.
Регистрация и использование
Зарегистрировать дополнительный matcher можно при помощи метода jasmine.addMatchers(matchers)
, где matchers
является объектом. Каждый ключ этого объекта станет matcher’ом. Как только matcher зарегистрирован, он становится доступным для всех проверок.
Пример
Оба теста должны успешно пройти.
var myMatchers = {
toBeFasterThen: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
var result = {};
if (!expected || !expected.speed) {
throw new Error('Expected object doesn\'t have speed property'); // Кривые объекты даже не проверяем.
// Пусть разработчик сразу видит некорректность входных данных
}
if (!actual || !actual.speed) {
throw new Error('Actual object doesn\'t have speed property');
}
result.pass = (actual.speed > expected.speed);
if (result.pass) { // Сообщение для случая ошибки проверки .not.toBeFasterThen
result.message = 'Expected ' + actual.name + '(' + actual.speed + ')' + ' not to be faster then ' + expected.name + '(' + expected.speed + ')';
} else { // Сообщение для случая ошибки проверки .toBeFasterThen
result.message = 'Expected ' + actual.name + '(' + actual.speed + ')' + ' to be faster then ' + expected.name + '(' + expected.speed + ')';
}
return result;
}
};
}
};
describe('My new matcher "toBeFasterThen"', function() {
var animals = [
{name: 'cow', speed: 4},
{name: 'monkey', speed: 8},
{name: 'donkey', speed: 6},
{name: 'horse', speed: 40}
];
beforeEach(function() {
jasmine.addMatchers(myMatchers); // регистрируем новый matcher
});
it('is a donkey faster than a cow', function() {
expect(animals[2]).toBeFasterThen(animals[0]);
});
it('is a monkey slower than a horse', function() {
expect(animals[1]).not.toBeFasterThen(animals[3]);
});
});
Заключение
Вот и всё! Мы сделали такую проверку, которой нет во встроенных в Jasmine. С помощью этого подхода можно сделать свои unit-тесты ещё более читабельными и понятными, а также уменьшить дублирование кода. Думаю, прочитав эту статью, ты сможешь сделать проверки гораздо сложнее, решающие реальные задачи и упрощающие твои unit-тесты.
Если тебе понравилась статья, буду признателен, если ты поделишься ссылкой с неравнодушными к данной теме друзьями в соц. сетях и оставишь комментарий с отзывом или вопросом к материалу статьи.