Картинка для привлечения внимания:
Я — начинающий веб-разработчик. И не так давно мне захотелось научиться работать так, как это делают настоящие программисты.
Под этим я понимал 3 основных элемента:
- Использование системы контроля версий.
- Грамотное комментирование кода.
- TDD или хотя бы простое юнит-тестирование кода.
Для первого пришлось освоить азы git, и создать свой первый репозиторий на github. Для второго выбрал JsDoc, из-за которого пришлось перебраться с notepad++ на sublime text (только там был соответствующий плагин).
А вот с третьим, неожиданно для меня, возникли серьёзные трудности.
Так как я очень уважаю jQuery (кстати первый свой репозиторий я как раз и открыл для написания плагина для jq) выбор фреймворка для юнит-тестирования пал на Qunit что оказалось роковой ошибкой. И столкнулся со следующим:
- Сбивается основная вёрстка из-за того что для работы Qunit надо вставлять немаленький кусок HTML кода на страницу. И все результаты тестов отображаются там же. Да это можно исправить костылями с position:absolute, opacity:.5 и подобными, но я не хочу бороться с фреймворком, я хочу им пользоваться
- Тесты неоднозначны. Я несколько раз натыкался на случай, когда, нажимая F5, получал разное количество пройденных и проваленных тестов. А у меня в коде ни асинхронности, ни работы с куки, ни аякс-запросов. Если я правильно понимаю то это из-за киллер-фичи изменения последовательности тестов, в зависимости от предыдущего результата — проваленные тесты проверяются в первую очередь.
- Fail тестов чрезвычайно ненагляден. qUnit практически всегда выводит toString() одного из переданных аргументов, игнорируя второй. Изредка выводит и второй, а бывают ситуации, когда правильно сработает diff. Я понимаю что diff — очень сложная функция, и стоит радоваться тому что она хоть иногда срабатывает, но что мешает Qunit выводить аргументы при каждом fail — для меня загадка.
Мне всё это весьма не нравилось, но осознание того, что большие дяди и тёти так работают, не давало мне опустить руки. Я надеялся, что когда завершу разработку своего плагина, мне откроется какой-то сакральный смысл всего того, с чем мне приходилось сталкиваться во время его тестирования. Неделю я кололся, плакал, но продолжал есть кактус, сражался с тяжёлой формой болезни я-знаю-как-надо. Но мои нервы не железные и, в конце концов, я расплакался как маленькая девочка сдался и написал свой велосипед.
А теперь о нём поподробнее
test.it — фреймворк для тестирования JavaScript кода.
Для тех, кому текст не интересен — ссылка на репозиторий на github: test.it
Основные особенности:
- Для отображения результата используется консоль.
- Последовательность тестов не изменяется.
- В результаты всех тестов включён массив полученных аргументов.
- Поддерживается группировка тестов без ограничений на уровень вложенности.
- При возникновении ошибки внутри группы (например, в одном из тестов), она отлавливается и отображается в результате, не нарушив работу остального кода.
Последнее я подглядел у Qunit, в этом они, конечно, молодцы.
На текущий момент реализованы тесты на:
- Равенство двух аргументов. Аналог ok из Qunit.
- Не-ложный результат аргумента (не NaN,Null,undefined,0,false,[],'') другими словами — прохождение if(). Близкий аналог equal из Qunit.
Простое начало
Для того что бы подключить фреймворк — достаточно всего лишь добавить строку
<script src='path/to/testit.js'></script>
куда вам вздумается в конец тега <body>.
А начать использование можно, очевидно, с первого теста:
test.it('first test');
Тест — функция (метод объекта test) которая проверяет выполнение какого-либо условия.
Функция test.it(entity) проверяет существование entity и не-ложность его значения.
Открыв консоль (Firebug или Chrome, как обладающие максимальной поддержкой данного API) мы увидим следующее:
Не густо (:
А всё потому, что мы не завершили тест вызовом test.done(). Сделаем это. Теперь наш код выглядит так:
test.it('first test');
test.done();
А в консоли соответственно:
Что означает, что все тесты пройдены.
root — корневой элемент, или группа тестов нулевого уровня. Он обладает теми же свойствами что и любая другая группа, но о них будет подробнее чуть позже.
Если раскрыть элемент root мы увидим статистику прохождения всех тестов внутри него, и время их выполнения.
Очевидно что:
- pass — количество пройденных
- fail — количество проваленных
- error — количество завершившихся ошибкой выполнения
Статистика разделена на тесты и группы. Но наверняка в ближайших релизах она упроститься, и это разделение пропадёт.
Последняя строка: pass: no comment — наш тест. Развернём его и посмотрим подробнее:
Первым делом идёт метка о том, что тест пройден. Вообще они бывают трёх видов:
- pass — пройден
- fail — провален
- error — завершился ошибкой, которая не повлекла за собой сбоя работы остального кода
(например было передано 0 или >2 аргументов)
Далее no comment — комментарий, который мы ещё не задали. Чуть ниже рассмотрим, как это можно сделать.
Следующая строка «argument exist and not false» — описание того, что необходимо для прохождения теста.
И напоследок массив полученных аргументов, в нашем случае из одного элемента ″first test″
Что бы не видеть больше этого неприятного no comment, добавим к нашему тесту комментарий.
test.it('first test');
test.comment('Простая проверка');
test.done();
Теперь результат выглядит так:
Но все что мы только что писали, в принципе, ничего особо не тестировало. Давайте исправим ситуацию и добавим настоящий тест.
test.it('first test');
test.comment('Простая проверка');
var Me = {name:'Titulus',lastName:'Desiderio'};
test.it(Me);
test.comment('Я существую?');
test.done();
Теперь в консоли:
Можем рассмотреть второй тест поподробнее. Развернём его, и Object — единственный аргумент, в массиве аргументов.
Как вы видите, тест проверяет всего лишь существование, и не-ложность значения единственного переданного ему аргумента. Но это не мешает иногда подглядывать в этот объект, что бы лишний раз напомнить себе, что именно мы передали, и получить дополнительную, полезную в некоторых случаях, информацию.
Предыдущие тесты были успешно пройдены, давайте поставим задачу посложнее.
test.it('first test');
test.comment('Простая проверка');
var Me = {name:'Titulus',lastName:'Desiderio'};
test.it(Me);
test.comment('Я существую?');
test.it(Me.habr);
test.comment('Хабр?');
test.done();
Как вы заметили, проваленный тест, и соответствующая проваленная группа (root) были раскрыты по умолчанию.
Кстати, благодаря массиву аргументов, мы сразу видим, почему тест был провален — переданный аргумент не определён.
Теперь осталось исправить ситуацию:
test.it('first test');
test.comment('Простая проверка');
var Me = {name:'Titulus',lastName:'Desiderio'};
test.it(Me);
test.comment('Я существую?');
Me.habr = 'Хабрахабр!';
test.it(Me.habr);
test.comment('Хабр?');
test.done();
И вновь старая картина!
Давайте ещё усложним задачу. Вместо проверки на существование и не-ложность, проверим на правильность значения.
test.it('first test');
test.comment('Простая проверка');
var Me = {name:'Titulus',lastName:'Desiderio'};
test.it(Me);
test.comment('Я существую?');
Me.habr = 'Хабрахабр!';
test.it(Me.habr);
test.comment('Хабр?');
test.it(Me.habr,'habrahabr.ru');
test.comment('адрес хабра');
test.done();
функция test.it(entity1, entity2) — проверяет равенство между entity1 и entity2.
Посмотрим в консоль:
Тест провален, чего и следовало ожидать. Глядя на результат, причина провала вполне очевидна. Исправив
Me.habr = 'Хабрахабр!';
на
Me.habr = 'habrahabr.ru';
Мы опять получаем
Need to go deeper
Разберёмся в упомянутых ранее группах.
Группа —
- набор тестов (или групп);
- метод объекта test который этот набор создаёт;
- собственно сам этот набор в виде JavaScript-объекта.
Помимо массива тестов и групп он несёт в себе статистику пройденных, проваленных и вызвавших ошибку тестов (или групп), а так же время, потраченное на их выполнение.
Рассмотрим следующий код:
test.it(2>1);
test.comment('Работают ли тут законы математики?');
test.group('первая группа',function(){
test.it(2>1);
test.comment('А тут?');
});
test.done();
С test.it(2>1); и так всё понятно, но что делает test.group?
Функция test.group(groupname, fun) — создаёт новую подгруппу для тестов, других групп и прочего кода. Имя берётся из аргумента groupname. А функция fun будет пытаться выполниться, и если внутри неё произойдёт ошибка — это не прервёт работу остального кода. Ошибка будет помещена в поле error данной группы.
Раскроем root:
Вот она наша группа первая группа, оформлена так же как root, только имя то — что мы задали.
Раскроем и её.
Никаких особых отличий от root нет и быть не должно.
Тут стоит только обратить внимание на статистику в root и в нашей группе.
! Важный момент: статистика отображает только результаты на данном уровне. Пусть у нас и написано 2 тест, но у root в статистике тестов виден только один пройденный.
Добавим туда ещё пару-тройку тестов.
test.it(2>1);
test.comment('Работают ли тут законы математики?');
test.group('первая группа',function(){
test.it(2>1);
test.comment('А тут?');
test.it(1, Number(1));
test.comment('равна ли еденица самой себе');
test.it(h.a.b.r);
test.comment('А что ты сделаешь с несуществующим аргументом?');
test.it(2+2,4);
test.comment('проверим на знание школьного курса');
});
test.it(1<2);
test.comment('А теперь?');
test.done();
И увидим
что тест на 2+2=4 даже не был запущен, потому что предыдущий с h.a.b.r не был выполнен из-за ошибки ReferenceError, которая была аккуратно выведена под статистикой, до тестов. Но при этом последний тест на 1<2 — проходит, потому что он был вне группы, с ошибкой.
Обработка ошибок — одна из приоритетных задач, которую я перед собой на сегодняшний день ставлю. Так что не удивляйтесь, если данный пример потеряет свою актуальность после ближайших релизов. Но основная идея отлова ошибок, без аварийного завершения программы, останется.
И последний момент касательно групп. Многоуровневая вложенность!
test.group('need',function(){
test.group('to',function(){
test.group('go',function(){
test.group('deeper',function(){
test.it('bye habr');
test.comment('bye bye');
});
});
});
});
test.done();
Под капотом
Весь код доступен на github, так что можете его читать, комментировать, форкать, предлагать пуллреквесты и т.п. Лицензия MIT, хотя подумываю о переходе на WTFPL.
Хотел остановиться подробнее на test.root — объект соответствующий группе нулевого уровня. Именно его заполняют тесты, а потом уже _printConsole() его парсит и наводит всю эту красоту.
{
"type": "group",
"name": "root",
"status": "pass",
"time": 7,
"result": {
"tests": {
"passed": 1,
"failed": 0,
"error": 0,
"total": 1
},
"groups": {
"passed": 1,
"failed": 0,
"error": 0,
"total": 1
}
},
"stack": [
{
"type": "test",
"status": "pass",
"comment": "Работают ли тут законы математики?",
"description": "argument exist and not false",
"time": 0,
"entity": [
true
]
},
{
"type": "group",
"name": "первая группа",
"status": "pass",
"time": 1,
"result": {
"tests": {
"passed": 1,
"failed": 0,
"error": 0,
"total": 1
},
"groups": {
"passed": 0,
"failed": 0,
"error": 0,
"total": 0
}
},
"stack": [
{
"type": "test",
"status": "pass",
"comment": "А тут?",
"description": "argument exist and not false",
"time": 0,
"entity": [
true
]
}
]
}
]
}
Кстати да. мне очень стыдно, но есть часть стороннего кода — функция deepCompare() не доступная извне. Она сравнивает два аргумента любых типов. Взял её тут.
И большой спасибо mkharitonov за подсказку в реализации многоуровневой вложенности.
Обратная сторона медали
Конечно, есть недостатки. Некоторые серьёзные, некоторые не очень. Надеюсь, раз код opensource, сообщество поможет мне их минимизировать, или превратить в достоинства.
Ключевые:
- Не кроссбраузерно. Console API толком поддерживают только Google Chrome (и прочие на основе chromium) и плагин Firebug под firefox. Говорят ещё Safari, но у меня нету устройств Apple для проверки.
- Нет возможности запустить отдельно одну группу тестов.
Есть костыль — ставить test.done() не в конце всего кода, а в конце той группы, которую надо протестировать. Но это костыль, и он не обладает всем необходимым функционалом.
решаемо - На текущий момент нету diff, и это решаемо.
- Нет тестов на ajax, и это опять же решаемо.
Что дальше?
А дальше будут названные выше изменения вывода статистики.
Улучшения обработки и вывода ошибок.
Улучшения вывода тестов — добавятся нумерация и подсказки для проваленных тестов.
Добавятся новые тесты test.them, test.type, test.types, test.time. О них подробнее можете прочитать в README.
Будут исправляться недостатки, названные в предыдущем разделе.
Ещё раз ссылка на репозиторий на github: test.it
P.S. Пост будет перенесён в хаб «Я Пиарюсь», как только наберу достаточно кармы.
Автор: titulusdesiderio