Jasmine Custom Matchers

Сегодня темой нашей беседы станет 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-тесты.

Если тебе понравилась статья, буду признателен, если ты поделишься ссылкой с неравнодушными к данной теме друзьями в соц. сетях и оставишь комментарий с отзывом или вопросом к материалу статьи.