Слоган Nike очень четко описывает то, о чем я хочу поговорить в этой статье: модульное тестирование. В глубине души каждый знает, какие есть выгоды от модульного тестирования кода. Модульное тестирование согреет вам душу, когда вы будете ложиться спать, зная что изменения сделанные вами, не сломали ничего написанного раньше. Оно сделает вас счастливыми и даст вам спокойствие :)
Тем не менее, многие (включая меня) до сих пор не используют модульное тестирование кода. Этому есть несколько объяснений, но сейчас разговор не об этом. Сейчас я хочу показать вам, насколько легко тестировать модули в JavaScript. Я буду использовать для этого QUnit, написанный John Resig, создателем jQuery.
Настройка среды тестирования
Создадим HTML файл который будет загружать jQuery, Qunit и таблицу стилей.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="nl" lang="nl"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Юнит тесты библиотеки тэгов</title> <script src="http://code.jquery.com/jquery-latest.js"></script> <link rel="stylesheet" href="http://github.com/jquery/qunit/raw/master/qunit/qunit.css" type="text/css" media="screen" /> <script type="text/javascript" src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js"></script> </head> <body> <h1 id="qunit-header">Юнит тесты библиотеки тэгов</h1> <h2 id="qunit-banner"></h2> <h2 id="qunit-userAgent"></h2> <ol id="qunit-tests"></ol> </body> </html>
На этом этапе, когда вы запустите код в браузере, ничего особенного не произойдет, для начала надо добавить наши тесты.

Добавляем юнит-тесты
Теперь, когда мы все подготовили, пора написать тест. Я приведу вам несколько примеров того, как я делал это для своей библиотеки тэгов. Если вам необходимо API или вы хотите знать все возможности QUnit, вы можете посмотреть это здесь.
Что бы мой код был более организованным, я стараюсь повторить в тестах его структуру. Вот что я имею в виду:

Файловая структура
Все тесты лежат в отдельных файлах, отражающих оригинальную структуру. Тест для /tag/tag.pubsub.js находится в файле /tests/tag/tag.pubsub.js. Я думаю это хорошая идея - разделять тесты на модули. В одном большом файле, или даже внутри HTML файла - тесты все равно будут работать. Просто будет сложнее поддерживать код, если у вас много тестов.
Т.к. я везде использую модули, надо добавить признак того, что тестируется новый модуль.
$(function(){ /** * Опишем модуль: */ module("Базовый класс Tag"); });
Это файл /tests/tag.js и он подключен к HTML в нашем первом шаге:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="nl" lang="nl"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Юнит тесты библиотеки тэгов</title> <script src="http://code.jquery.com/jquery-latest.js"></script> <link rel="stylesheet" href="http://github.com/jquery/qunit/raw/master/qunit/qunit.css" type="text/css" media="screen" /> <script type="text/javascript" src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js"></script> <!-- добавим ссылки на юнит тесты: --> <script type="text/javascript" src="../tag.js"></script><!-- тестируемая библиотека--> <script type="text/javascript" src="tag.js"></script><!-- тест для этой библиотеки--> </head> <body> <h1 id="qunit-header">Юнит тесты библиотеки тэгов</h1> <h2 id="qunit-banner"></h2> <h2 id="qunit-userAgent"></h2> <ol id="qunit-tests"></ol> </body> </html>
В HTML, во-первых, мы подключаем файл содержащий тестируемый код. Далее подключаем тест этого кода. Вы можете добавить любые другие файлы для каждого тестируемого плагина.
И так, мы определили наш модуль. Во-первых, я добавим проверку того, загрузился ли файл, и было ли определенно пространство имен Tag в window.
/** * Описываем модуль: */ module("Базовый класс Tag"); /** * Проверяем существование объекта Tag: */ test("Tag", function() { expect(2); // необязательно equals(typeof window.Tag, "object", "Проверяем существование объекта Tag"); equals(typeof window.Tag.constructor, "function", "Проверяем тип конструктора"); });
Тесты для проверки существования объекта Tag сгруппированы вместе функцией test(). Внутри неё у нас может быть столько утверждений для проверки, сколько нам надо. В данном случае их два: проверка является ли window.Tag объектом и есть ли у него конструктор. Если оба утверждения пройдены успешно, это вполне достаточно убедит меня в том, что базовый класс Tag загружен и доступен на странице.
The basic premise of unit testing is that you test the smallest unit that makes up your code. In the case of the Tag library, the smallest unit will be a method from a plugin or the base class. So the next thing we’re obviously going to do, is test each of the methods inside the Tag object.
В качестве примера, напишем тест для метода Tag extend():
/** * Проверяем работу метода extend : */ test("Tag.extend()",function() { // утверждение использует ok() ok(typeof window.Tag.extend === "function","Метод существует"); // шаблон плагина (так сказать пустышка (mock)) var tmp = { _initCheck:false, init:function() { this._initCheck = true; } }; // Проверяем зарезервированные пространства имен: var reserved = ["extFN", "extLocalFN"]; $.each(reserved, function() { ok(!window.Tag.extend(this,tmp), "Пространство имен '" + this + "' не переопределенно"); }); // добавляем проверку плагина ok(window.Tag.extend("test",tmp), "Пространство имен 'test' есть"); ok(!window.Tag.extend("test",tmp), "Пространство имен 'test' нельзя объявлять дважды"); equals(typeof window.Tag.test, "object", "Плагин Tag.test существует"); equals(typeof window.Tag.fn.test, "object", "Тестируемый плагин добавлен к FN"); // проверяем, можно ли расширить функциональность плагина: $.each(reserved, function(index, ns) { var obj = window.Tag[ns]; $.each(obj,function(key, value){ same(window.Tag.test[key], obj[key], "Проверяем существование '" + key + "'"); }); }); // проверяем был ли вызван init(): equals(true, window.Tag.test._initCheck, "Init был запущен"); });
Метод extend() делает очень многое, и все это, естественно, надо протестировать. Это единственный способ убедиться, что метод делает все, что должен. Для сложных методов, с большим количеством способов выполнения, тестировать становиться не так просто. Это причина делать методы маленькими и выносить функционал в подфункции: это упрощает тестирование (и понимание, когда вы решите заглянуть в ваш код через полгода после его написания).
Юнит тесты Ajax вызовов
Плагин Tag.core имеет функционал с асинхронной загрузкой. Проблема в том, что AJAX выполняется асинхронно, а модульные тесты - синхронно. Тесты должны выполняться по порядку, ни один тест не должен выполняться параллельно с другим. Тем не менее асинхронные вызовы надо тестировать. В QUnit есть метод asyncTest(), который предоставляет такую возможность. Я его не использую, делаю это "вручную".
Мы должны остановить выполнение тестов на время выполнения асинхронных тестов. После завершения асинхронного теста, мы можем возобновить выполнение остальных тестов. Т.е. мы просто ждем пока асинхронный запрос не будет полностью выполнен, и продолжаем выполнение синхронных тестов. Ничего не должно выполняться параллельно.
/** * проверяем работу метода loadJS: */ test("Tag.core.loadJS()",function() { // проверяем загрузку: window.Tag.core.loadJS("dummy.js"); // когда тестируем асинхронную функциональность, мы должны временно остановить // выполнение тестов: stop(); window.setTimeout(function(){ equals(typeof window.dummy, "object", "Провряем, загружается ли dummy"); // стартуем тесты снова: start(); },250); });
Во-первых, последовательность тестов останавливается методом stop(). Как только тест пройден, метод start() восстановит нормальное выполнение последовательности тестов. Просто, но эффективно.
Метод asyncTest() в QUnit делает примерно то же самое. Он просто выполняет метод stop() за вас. Вам необходимо только указать когда вернуться к выполнению тестов.
Будьте внимательны, асинхронные тесты могут содержать только один асинхронный вызов. Т.е. не может выполняться два AJAX запроса в одном тесте.
test("Tag.core.loadJS() multiple",function() { // проверяем множественную загрузку window.Tag.core.loadJS(["dummy.js","dummy2.js"]); stop(); window.setTimeout(function(){ equals(typeof window.dummy, "object", "Провряем, загружается ли dummy"); equals(typeof window.dummy2, "object", "Провряем, загружается ли dummy повторно"); // стартуем тесты опять: start(); },250); });
Результаты тестов
Написав тесты, вы должно быть хотите увидеть какие-то результаты. Просто сохраните все и откройте html файл в браузере. Тесты запуститься и вы получите результаты, например, такие:

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

Чем подробнее вы опишите ваши тесты, тем проще вам будет понять где произошла ошибка.
Заключение
Использование модульного тестирования функционала JavaScript не может быть проще. Входного порога по уровню знаний нет, это так же легко, как создать html файл. Написание тестов может принести немало пищи для ума. В начале вы будете делать ошибки, но это нормально. Чем дольше вы будете использовать написание тестов, тем эффективнее вы будете их писать. Но надо начать. Просто начать.
Помните, я только показал дверь. войти вы должны сами. :)
Весь код используемы здесь можно найти в репозитарии


0 Responses to Юнит-тестирование в Javascript: просто делайте это!