Одним дождливым осенним вечером пришла мне в голову мысль о том, что никогда прежде я не писал JavaScript код следуя канонам test-driven development (TDD). Лиха беда начало! Результатом работы стала маленькая библиотека-шаблонизатор работающая по принципу «JSON на входе, HTMLElement или просто DOM объект на выходе».
Из инструментов использовались: CoffeeScript, QUnit, PhantomJS, Google Closure compiler, а собирается всё это с помощью старого доброго GNU Make. Статья для всех, кому интересна библиотека и для тех, кто поверхностно знаком с вышеперечисленными технологиями и хотел бы увидеть их в работе.
Что получилось в результате?
elem = kidomi(
['div#main.content',
['span', {style: {color: 'blue'}}, 'Select file'],
['form', {
name: 'inputName',
action: 'getform.php',
method: 'get'},
'Username: ',
['input', {'type': 'text', 'name': 'user'}],
['input', {'type': 'submit', 'value': 'Submit'}]]])
Где elem
— это объект HTMLElement
, который выглядит как:
<div id="main" class="content">
<span style="color: blue;">Select file</span>
<form name="inputName" action="getform.php" method="get">
Username:
<input type="text" name="user"></input>
<input type="submit" value="Submit"></input>
</form>
</div>
Ещё один пример, в котором сначала создаётся элемент <a>
, к onclick
которого привязывается функция, после чего элемент добавляется в общую структуру:
button = kidomi(['a.button', {href: '#'}]);
button.onclick = function() { alert('Hello world!'); };
elem = kidomi(['div', ['span', 'Click this button:'], button]);
Бывалые люди сразу вспомнят jquery-haml, однако вдохновение для написания kidomi черпалось из ClojureScript-библиотеки dommy.
О чём же сыр-бор?
- kidomi написана на CoffeeScript.
- Она компилируется Google Closure в расширенном (ADVANCED_MODE) режиме.
- Она покрыта юнит-тестами.
- И эти тесты работают в т.ч. с помощью PhantomJS.
- Всё это собирается и запускается с помощью make.
Тонкости CoffeeScript
Исходный код начинается следующим образом:
window['kidomi'] =
kidomi = (data) ->
...
Для тех, кто не знаком с особенностями компиляции CoffeeScript: по-умолчанию весь скомпилированный код оборачивается в функцию-обёртку и таким образом не экспортируется глобально. Кстати это поведение можно отключить флагом компилятора --bare
, но разве позволительно засорять глобальное пространство имён?
(function() {
/* ... */
window['kidomi'] = kidomi = function(data) {
/* ... */
}
/* ... */
}).call(this);
Ещё одна особенность записи:
window['kidomi'] =
# а не
window.kidomi =
Это сделано специально для компилятора Google Closure, который бы «сократил» название, при записи window.kidomi =
Далее, код пишется в похожем ключе:
kidomi.makeElementFromTagData =
makeElementFromTagData = (tagData) ->
# ...
kidomi.addAttributes =
addAttributes = (elem, data) ->
# ...
# и т.д.
Как вы могли заметить, функции объявляются как локально, так и «экспортируются» в функцию-объект kidomi
. В первом случае это сделано для удобства: не нужно писать никаких префиксов (хотя в CoffeeScript достаточно написать @name
, что скомпилируется в this.name
). A чтобы юнит тесты могли до этой функции добраться, её можно записать в виде аттрибута глобального объекта. Что и делается через kidomi.functionName
.
Тестирование
Помните свои первые шаги в TDD? Мне например стоило неимоверных усилий заставить себя сначала писать тесты, а после — код. Зато, как быстро TDD приносит дивиденды!
Как было сказано выше, для написания юнит тестов для kidomi использовалась библиотека QUnit. Один из простейших тестов выглядит следующим образом:
test('isString', ->
ok(kidomi.isString(''))
ok(not kidomi.isString({}))
ok(not kidomi.isString([]))
ok(not kidomi.isString(10)))
А вот и сама функция:
kidomi.isString =
isString = (s) ->
typeof(s) == 'string' or s instanceof(String);
Стоит обратить внимание на то, что необходимо протестировать не только kidomi.js, но и обработанную напильником компилятором Closure kidomi.min.js. В идеале — все тесты покрывающие несжатый файл должны работать и для сжатой версии. Но тут мы натыкаемся на то, что все имена фунцкий кроме kidomi
были изменены до неузнаваемости. Например, вышеприведённый код isString(s)
превратился в
d.e=k=function(a){return"string"===typeof a||a instanceof String};
Чтобы с этим справиться, нужно скомпилировать библиотеку и тесты как единое целое. Также нужно указать компилятору, что qunit.js — это внешняя зависимость и соответственно такие имена фунцкий, как test
, module
, ok
и т.д. должны остаться без изменений.
Тем не менее, тестирование, в котором библиотека и тесты слиплены в один min.js файл, всё-таки отличается от тестирования сжатой библиотеки отдельно. Один из вариантов — запустить тесты лишь для основной функции.
Таким образом полное тестирование kidomi происходит в 3 прохода:
- Все тесты прогоняются на несжатой kidomi.js. Тесты и библиотека в отельных файлах.
- Все тесты сжимаются вместе с kidomi.js. Тесты и библиотека в одном файле.
- Тесты для функции
kidomi()
прогоняются на сжатой kidomi.min.js. Тесты и библиотека в отельных файлах.
PhantomJS
Как уже рассказывалось на Хабре, PhantomJS — это WebKit работающий в консоли и управляющийся собственным JS-API. На просторах интерета был найден скрипт связывающий PhantomJS и QUnit простым и в то же время эффективным способом: он парсит страницу с результатами тестирования и завершает процесс с кодом 0 (успех) или 1 (ошибка) в зависимости от результата тестов. Кстати, все тесты можно запустить в обычном браузере.
Сборка
Для сборки можно было использовать Rake, Maven, Grunt и т.д., но к сожалению со всеми вышеперечисленными системами я на «вы» (камрады, обещаю наверстать упущенное к следующему посту на тему JavaScript). Make же, как мне кажется, справился с задачей на «Ура!».
Makefile состоит всего из трёх основных целей сборки (build targets): ${BUILD_DIR}, $(BUILD_DIR)/kidomi.js
и $(BUILD_DIR)/kidomi.min.js
(также дополнительные плюшки в виде целей all, clean, .PHONY
и т.д.). В конце Makefile'а подключается файл Makefile.testsuite.mk
содержащий цели и правила для сборки и запуска всех ранее упомянутых тестов.
Заключение
Надеюсь, статья была для вас интересной и вы узнали из неё что-то новое. Исходный код kidomi открыт для всех желающих. Архивы содержат собранную версию библиотеки. Все комментарии, советы, отзывы и критика горячо приветствуются!
Благодарю за внимание!
Автор: BasicWolf