Не забывайте про тестирование на стороне клиента!

Проведя надлежащим образом тестирование вашего приложения, вы сможете уменьшить количество ошибок в коде, облегчить дальнейшую отладку приложения, а также обеспечить хорошо структуру кода.

При модульном тестировании приложения на стороне клиента возникают совсем другие проблемы, чем при тестировании на стороне сервера. Когда вы имеете дело с кодом на стороне клиента, вам постоянно приходится разделять логику приложения и логику DOM, а также структурировать код JavaScript в целом.

К счастью, существует много библиотек для тестирования на стороне клиента, которые помогут протестировать код, создать метрики диапазонов охвата тестов, а также проанализировать сложность всего кода.

Зачем нужны тесты в принципе?

Прежде всего, общее модульное тестирование является методом, обеспечивающим снижение количества ошибок, гарантирующим то, что ваши прикладные функции будут работать надлежащим образом.

Кроме того тесты являются неотъемлемой частью таких понятий, как Разработка через тестирование (TDD) и Разработка, основанная на функционировании (BDD).

Эти две стратегии модульного тестирования помогают разработать приложение, путем написания теста до описания самой логики приложения. Разрабатывая тесты до написания кода, вы получаете возможность хорошо продумать структуру своего приложения.

Причиной этого является то, что когда вы пишете тесты, вы, как правило, пытаетесь, спроектировать, как интерфейс приложения будет взаимодействовать с кодом. Благодаря этому вы можете получить лучшее понимание конструкции приложения.

Предварительное тестирование быстро укажет вам на любые недостатки в вашем приложении, потому что вы пишете тестовый код, используя фактический код будущего приложения!

TDD – это процесс исследования кода

Вы поймете, что TDD поможет вам исследовать свой код в процессе его написания. TDD производится по принципу «Красный свет, Зеленый свет, Рефакторинг».

Этот принцип предполагает, что в первую очередь создается код, достаточный для запуска теста и определения ошибки. Затем вы пишете код, который позволяет пройти тест.

После этого, вы думаете над тем, что вы только что написали и как этот код реорганизовать. Просто и понятно. BDD несколько отличается от этого подхода, он больше сосредоточен на конкретных требованиях и спецификациях бизнеса.

Тестирование на стороне клиента

Есть много причин того, почему вы должны осуществлять тестирование кода на стороне клиента. Как упоминалось ранее, это поможет уменьшить количество ошибок и поможет вам в разработке приложения.

Тестирование клиентской части имеет важное значение еще и потому, что это дает вам возможность проверить код интерфейса, отдельно, без привязки к представлению. Иными словами, одним из его преимуществ является то, что вы получаете возможность протестировать код JavaScript, фактически не запуская приложение на сервере.

Вы просто запускаете тесты и проверяете, работает ли приложение без задействования функциональных элементов. Часто вам даже не нужно иметь доступ к Интернету, пока вы не настроите тесты правильно.

Учитывая, какую огромную роль играет JavaScript в современном веб-дизайне, важно знать, как проверить код и уменьшить вероятность ошибки в рабочем коде. Вашему боссу не понравится, если это случится, и это не должно случиться!

На самом деле можно начать тестирование клиентской стороны с написания тестов, связанных с отчетами об ошибках. Это позволит вам получить необходимую практику в написании тестов, если до этого такого опыта у вас не было, и вам практически приходится начинать с нуля.

Еще одна причина тестировать структуру кода на стороне клиента заключается в том, что создав набор тестов один раз, вы сразу охватываете значительную часть кода. И когда вы готовы идти и добавлять новые функции, вы сможете добавить новую функцию, повторно запустить тесты и проверить, не потеряны ли некоторые существующие функции, все ли они работают правильно.

Приступим к работе

Начиная работу по тестированию на стороне клиента, вы можете столкнуться с рядом сложностей. Если вы никогда не делали этого раньше. Одной из самых трудных задач в тестировании на стороне клиента является определение того, как лучше всего изолировать DOM от логики приложения. Это часто означает, что вам нужно в определенном смысле абстрагироваться от DOM.

Проще всего добиться этого, используя фреймворки клиентской стороны, такие как Knockout.js, Backbone.js или Angular.js. Это только некоторые из них.

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

Совсем необязательно производить модульное тестирование, работая только с открытым JavaScript. Вам будет намного легче, если вы будете проектировать код, абстрагировавшись от DOM.

Выбор библиотеки для тестирования

Существует большое количество различных библиотек для тестирования, хотя три из них выделяются особо: QUnit, Mocha и Jasmine.

Jasmine and Mocha принадлежат к BDD школе модульного тестирования, в то время как QUnit – это база модульного тестирования как таковая.

Далее в этой статье мы будем освещать тему на примере QUnit, так как он позволяет приступить к тестированию на стороне клиента очень просто.

TDD с помощью QUnit

Начать работу с QUnit очень просто. В приведенном ниже HTML-коде содержится все, что нам нужно:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>QUnit Example</title>
    <link rel="stylesheet" href="qunit.css">
</head>
<body>
    <div id="qunit"></div>
    <div id="qunit-fixture"></div>
    <script src="qunit.js"></script>
    <script src="../app/yourSourceCode.js"></script>
    <script src="tests.js"></script>
</body>
</html>

Для нескольких следующих примеров давайте предположим, что мы создаем небольшой виджет, через который вы вводите в текстовое поле почтовый индекс, и виджет с помощью Geonames выводит нам соответствующие этому индексу город, штат и округ.

Сначала выводимое значение отображается просто как число, однако, как только вы ввели пять символов почтового индекса, извлекаются данные из Geonames.

Если данные найдены, то в виджете будет выведено несколько дополнительных полей: город, штат и округ. Мы также будем использовать Knockout.js. Первым этапом является создание теста ошибок.

Дизайн мы продумываем немного раньше создания первого теста, он, вероятно, должен содержать не менее двух ViewModels, с этого можно начать. Сперва мы определяем модуль QUnit и первый тест:

module("вывод почтового индекса");
test("view models должны существовать", function() {
    ok(FormViewModel, "viewModel для нашей формы должен существовать");
    ok(AddressViewModel, "viewModel для нашего адреса должен существовать");
});

Если вы запустите этот тест, он не будет пройден, теперь вы можете перейти к написанию кода, который позволит пройти его:

TDD с помощью QUnit
var AddressViewModel = function(options) {

};

var FormViewModel = function() {
    this.address = new AddressViewModel();
};

Теперь вы увидите зеленый, а не красный цвет. Такие тесты могут показаться вначале немного глупыми, однако их польза заключается в том, что они заставляют вас, по крайней мере, задумываться на ранних стадиях разработки:

TDD с помощью QUnit - 2

Следующий тест, который мы напишем, будет проверять функционал AddressViewModel. Из спецификации этого виджета мы знаем, что другие поля должны быть сначала скрыты, пока не будут найдены данные, соответствующие введенному почтовому индексу:

module("модель вывода адреса");
test("должны выводиться данные города, штата, если индекс был найден ", function() {
    var address = new AddressViewModel();

    ok(!address.isLocated());

    address.zip(12345);
    address.city("foo");
    address.state("bar");
    address.county("bam");

    ok(address.isLocated());
});

Ни одного фрагмента кода для этой функции еще не было написано, но идея заключается в том, что isLocated будет вычисляться через просмотр данных, и значение true будет возвращаться, только если индекс, город, штат и округ существуют.

Таким образом, этот тест, конечно, сначала не будет пройден. Теперь давайте составим код, чтобы пройти его:

var AddressViewModel = function(options) {
    options = options || {};

    this.zip = ko.observable(options.zip);
    this.city = ko.observable(options.city);
    this.state = ko.observable(options.state);
    this.county = ko.observable(options.county);

    this.isLocated = ko.computed(function() {
        return this.city() && this.state() && this.county() && this.zip();
    }, this);

    this.initialize();
};

Теперь, если вы запустите тест снова, вы увидите зеленый цвет!

Это концепция того, как можно использовать TDD для составления тестов интерфейса. В идеале, после каждого проваленного теста вы должны были бы составлять простейший код, который позволит пройти этот тест, а затем возвращаться к рефакторингу кода.

Вы можете узнать намного больше о практике TDD, и я предлагаю вам изучить другие источники по этой теме. Но приведенных примеров должно быть достаточно, чтобы дать вам общее представление о составлении предварительных тестов.

Псевдо зависимости с помощью Sinon.js

Sinon.js – это библиотека JavaScript, которая предоставляет скрытые, частичные и псевдо объекты JavaScript.

При составлении юнит-тестов вы должны обеспечить тестирование только данного "юнита" кода. Это часто означает, что вы должны будете создать некие псевдо или разорванные (заглушенные) зависимости, чтобы изолировать тестируемый фрагмент кода.

Sinon имеет предельно простые инструменты для этих целей. Geonames API поддерживает извлечение данных через точку назначения JSONP, что означает, что мы можем использовать $.ajax.

В идеале ваши тесты вовсе не будут зависеть от GeoNames API. Сервис может упасть, может умереть Интернет, канал может быть просто слишком медленным, чтобы выполнять запросы Ajax. Sinon решит эти проблемы:

test("нужно только попытаться извлечь данные, если введено 5 символов индекса ", function() {
    var address = new AddressViewModel();

    sinon.stub(jQuery, "ajax").returns({
        done: $.noop
    });

    address.zip(1234);

    ok(!jQuery.ajax.calledOnce);

    address.zip(12345);

    ok(jQuery.ajax.calledOnce);

    jQuery.ajax.restore();
});

В этом тесте совершается несколько действий. Прежде всего, функция sinon.stub непосредственно получает доступ к jQuery.ajax, чтобы отслеживать, когда он должен вызываться, как и другие операторы.

Исходя из того, что условие теста гласит: "нужно только попытаться извлечь данные, если введено 5 символов индекса", мы можем заключить, что до тех пор, пока введено только "1234", Ajax не должен вызываться ни разу. Затем, когда введено пять символов, должен быть запущен вызов Ajax.

После этого нам нужно восстановить исходное состояние jQuery.ajax, потому что мы стараемся придерживаться канонов модульного тестирования и хотим, чтобы наши тесты были автономны. Обеспечение автономности тестов важно для того, чтобы один тест не зависел от другого, и у них не было общих операторов. Они могут запускаться в любом порядке, и это не повлияет на результаты тестов.

Теперь, когда тест написан, мы можем запустить его, и посмотреть, из-за чего он не проходит, чтобы затем все же выполнить Ajax запрос к Geonames:

AddressViewModel.prototype.initialize = function() {
    this.zip.subscribe(this.zipChanged, this);
};

AddressViewModel.prototype.zipChanged = function(value) {
    if (value.toString().length === 5) {
        this.fetch(value);
    }
};

AddressViewModel.prototype.fetch = function(zip) {
    var baseUrl = "http://www.geonames.org/postalCodeLookupJSON"

    $.ajax({
        url: baseUrl,
        data: {
            "postalcode": zip,
            "country": "us"
        },
        type: "GET",
        dataType: "JSONP"
    }).done(this.fetched.bind(this));
};

Здесь мы прописываем отслеживание изменений вводимых данных почтового индекса. Всякий раз, когда они изменяются, будет вызываться метод zipChanged. Метод zipChanged будет проверять, достигла ли длина введенного индекса 5 символов.

Когда вводимое значение достигает 5 символов, будет называться метод fetch. Вот где в дело вступает заглушка Sinon. В этот момент $.ajax на самом деле представляет собой заглушку Sinon. Поэтому calledOnce в тесте будет принимать значение true.

Заключительный тест мы напишем для операции возврата данных от сервиса GeoNames:

test("должен устанавливать информацию о городе, основываясь на результатах поиска", function() {
    var address = new AddressViewModel();

    address.fetched({
        postalcodes: [{
            adminCode1: "foo",
            adminName2: "bar",
            placeName: "bam"
        }]
    });

    equal(address.city(), "bam");
    equal(address.state(), "foo");
    equal(address.county(), "bar");
});

Этот тест будет проверять, как данные с сервера извлекаются в набор AddressViewmodel. Запустите его, и вы увидите красный свет. Теперь нужно сделать так, чтобы тест был пройден:

AddressViewModel.prototype.fetched = function(data) {
    var cityInfo;

    if (data.postalcodes && data.postalcodes.length === 1) {
        cityInfo = data.postalcodes[0];

        this.city(cityInfo.placeName);
        this.state(cityInfo.adminCode1);
        this.county(cityInfo.adminName2);
    }
};

Метод просто устанавливает, что в данных с сервера существует массив postalcodes, а затем устанавливает соответствующие свойства для viewModel.

Видите, как это просто? Как только вы проделаете это один раз, вам захочется и в других случаях прибегать к помощи TDD. Возможно, вы начнете с небольших функций, которые легко тестировать. Вы будете чаще задумываться над тем, как код взаимодействует с различными зависимостями.

Теперь у вас будет набор тестов, которые вы сможете запускать каждый раз, когда к вашему приложению предъявляются какие-либо новые требования. Даже если вы пропустите что-то, и в код закрадется ошибка, вы сможете просто добавить новый набор тестов, чтобы исправить ее! В конце концов, вы получите полностью готовый к употреблению код.

Диапазон охвата теста

Диапазон охвата теста позволяет оценить, какая часть вашего кода была проверена при модульном тесте. Часто случается, что достичь 100% охвата довольно трудно, да этого и не требуется, но желательно сделать все возможное, чтобы диапазон охвата был как можно больше.

Одной из новейших и более простых библиотек для определения диапазона охвата является Blanket.js. Использовать ее с QUnit до смешного просто.

Вам всего лишь нужно взять код прямо с домашней страницы Blanket.js или установить библиотеку вместе с Bower. Затем добавить код в виде библиотеки в нижней части файла qunit.html, а затем добавить data-cover для всех файлов, для которых вы хотите установить диапазон охвата тестов:

<script src="../app/yourSourceCode.js" data-cover></script>
    <script src="../js/lib/qunit/qunit/qunit.js"></script>
    <script src="../js/lib/blanket/dist/qunit/blanket.js"></script>
    <script src="tests.js"></script>
</body>

Готово. Очень просто, и теперь в окне QUnit у вас появится индикатор охвата теста:

Диапазон охвата теста

В данном примере вы можете видеть, что диапазон охвата чуть-чуть не дотягивает до 100%. Но так как сам код небольшой, вы легко можете увеличить зону охвата. Вы можете развернуть панель и четко увидеть функции, которые еще не были охвачены:

Диапазон охвата теста - 2

В данном случае для FormViewModel в тестах не был создан экземпляр, и поэтому он не охвачен тестом. Вы можете просто добавить новый тест, который создает объект FormViewModel, и, возможно, вставить оператор, который проверяет, присутствует ли свойство address и представляет ли оно собой instanceOf для AddressViewModel.

После этого вы к своему удовольствию обнаружите, что диапазон охвата теста составляет 100%:

Сложные тесты

Так как ваши приложения будут становиться все больше и больше, было бы полезно иметь возможность запустить некоторый статический анализ кода JavaScript. Существует отличный инструмент для запуска анализа JavaScriptPlato.

Вы можете запустить plato, установив его через npm:

npm install -g plato

После этого вы можете запустить plato для папки, где находятся коды JavaScript:

plato -r -d js/app reports

Эта команда запускает Plato для всех файлов JavaScript, расположенных в папке "js/app" и выводит результаты в reports. Plato запускает все виды метрик для вашего кода, в том числе значения средних строк кода, вычисляемое значение стабильности, JSHint, сложность, исчисляемые ошибки и многое другое:

Сложные тесты

На приведенном выше рисунке показано не так уж много различных параметров – просто потому что код, с которым мы работали, содержится всего в одном файле. Но когда вы работаете с большим приложением, в котором много файлов и строк кода, вы сможете получить информацию, которая будет чрезвычайно полезной.

Plato даже отслеживает все запуски приложения, так что вы можете наглядно увидеть, как с течением времени меняется статистика.

Заключение

Тестирование на стороне клиента может показаться непростой задачей, поэтому на сегодняшний день уже разработано большое количество полезных инструментов, которые призваны сделать этот процесс проще.

Эта статья только поверхностно описывает средства, которые разработаны, чтобы облегчить тестирование на стороне клиента.
Это может быть трудоемкой задачей, но вы увидите, что, в конце концов, преимущества от применения наборов тестов и проверки кода перевесят все издержки.

Надеемся, что благодаря этой статье, вы уже в самое ближайшее время сможете самостоятельно приступить к тестированию кода на стороне клиента.