test.it — тестирование JavaScript или мой велосипед с вложенностью и подробным выводом

  • Tutorial
Внимание! В статье содержатся примеры работы до релизной (до v1.0.0) версии библиотеки.
Скоро выйдет ещё одна статья. Эта только в качестве ознакомления. Всю необходимую для использования библиотеки информацию можно получить в README и комментариях в коде.


Картинка для привлечения внимания:
test.it habrahabr
Я — начинающий веб-разработчик. И не так давно мне захотелось научиться работать так, как это делают настоящие программисты.
Под этим я понимал 3 основных элемента:
  1. Использование системы контроля версий.
  2. Грамотное комментирование кода.
  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 - pass
Что означает, что все тесты пройдены.

root — корневой элемент, или группа тестов нулевого уровня. Он обладает теми же свойствами что и любая другая группа, но о них будет подробнее чуть позже.
Если раскрыть элемент root мы увидим статистику прохождения всех тестов внутри него, и время их выполнения.
root
Очевидно что:
  • pass — количество пройденных
  • fail — количество проваленных
  • error — количество завершившихся ошибкой выполнения
Статистика разделена на тесты и группы. Но наверняка в ближайших релизах она упроститься, и это разделение пропадёт.

Последняя строка: pass: no comment — наш тест. Развернём его и посмотрим подробнее:
first test
Первым делом идёт метка о том, что тест пройден. Вообще они бывают трёх видов:
  • 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();
Теперь в консоли:
2 теста

Можем рассмотреть второй тест поподробнее. Развернём его, и Object — единственный аргумент, в массиве аргументов.
2 тест
Как вы видите, тест проверяет всего лишь существование, и не-ложность значения единственного переданного ему аргумента. Но это не мешает иногда подглядывать в этот объект, что бы лишний раз напомнить себе, что именно мы передали, и получить дополнительную, полезную в некоторых случаях, информацию.

Предыдущие тесты были успешно пройдены, давайте поставим задачу посложнее.
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();
fail
Как вы заметили, проваленный тест, и соответствующая проваленная группа (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();
И вновь старая картина!
root - pass

Давайте ещё усложним задачу. Вместо проверки на существование и не-ложность, проверим на правильность значения.
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.
Посмотрим в консоль:
Хабрахабр! != habrahabr.ru
Тест провален, чего и следовало ожидать. Глядя на результат, причина провала вполне очевидна. Исправив
Me.habr = 'Хабрахабр!';
на
Me.habr = 'habrahabr.ru';
Мы опять получаем
root - pass

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 — проходит, потому что он был вне группы, с ошибкой.

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

И последний момент касательно групп. Многоуровневая вложенность!
need to go deeper
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() его парсит и наводит всю эту красоту.
test.root для первого примера с группами:
{
    "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. Пост будет перенесён в хаб «Я Пиарюсь», как только наберу достаточно кармы.
Метки:
Поделиться публикацией
Похожие публикации
Комментарии 55
  • 0
    Печально, что только консоль — мобильные браузеры в пролете, например. Я бы предложил вынести собственно представление результатов в какую-то отдельную часть, чтобы можно было заменить ее на рендеринг HTML.
    • +2
      И чего ради вы зациклились на QUnit'e? Это далеко не единственная библиотека для TDD/BDD/юнит-тестов.
      • 0
        Именно по этому сначала заполняется test.root, а затем вывод в консоль строится по нему. Так что да другой вариант вывода вполне можно реализовать. Просто первоначальная цель стояла именно в выводе в консоль. Измените
            var _done = function(obj) {
                /** update time in root */
                root.time = new Date().getTime() - root.time;
        
                /** display root */
                // console.dir(root);
                _printConsole(root);
            }
        
        на
            var _done = function(obj) {
                /** update time in root */
                root.time = new Date().getTime() - root.time;
        
                /** display root */
                // console.dir(root);
                _printWhereEverYouWant(root);
            }
        
        Предварительно конечно написав метод _printWhereEverYouWant(root); — и наслаждайтесь результатом.

        Если есть более грамотный способ выделить вывод результатов, то с нетерпением жду pull-request или комментария об этом тут.
      • +5
        Прежде всего, чем не устроила связка Mocha + Chai, например?
        • +2
          Простите, но «Моча + Чай» — это звучит сильно! )) Такое впечатление, что соотечественники повсюду)
          • +8
            Любимый фреймворк Геннадия Петровича Малахова, говорят.
            • +2
              Mocha за авторством TJ Holowaychuk'а, так же из под его клавиатуры вышли expressjs, connect и кучка других популярнейших модулей для nodejs. Где вы тут соотечественника узрели?
              • +5
                "mocha" произносится как "мокка" или "мокке" в данном случае (вики). Это напиток на основе кофе.
                "chai" — это действительно чай, только от китайского "茶" ("ча"). Русское слово "чай" произошло от него же.
                • 0
                  Оно же мокко. Но мы как-то долго недоумевали от надписей на бумажном стаканчике: espresso, cappuccino, latte, mocha… Сошлись на том, что в этих стаканчиках, в принципе, можно сдавать анализы.
            • 0
              Говорят ещё Safari, но у меня нету устройств Apple для проверки


              можно взять устройство на Windows и поставить сафари на него
              support.apple.com/kb/DL1531
              • +1
                Это же 5.1.7. Apple перестала выпускать версии под Windows с 6-го релиза.
                • 0
                  тогда извиняюсь за дезинформацию — не обратил внимание на версию, а что Safari под Windows вообще прикрыли даже не знал
              • +1
                Любопытно, но:
                1. Мне кажется: argument existS всё таки
                2. Когда нет равенства аргументов (пример с url-ом хабра), то надо писать arguments are not equal — то есть состояние, а не цель проверки.
                • 0
                  1. спасибо, если бы не подруга, знающая английский, readme вообще было бы невозможно читать diff
                  2. когда я говорил про подсказки — это и имелось ввиду. Более того, подсказки будут интеллектуальными. Например если аргументы разных типов, в подсказке будет об этом речь.
                  • +1
                    по п1: у вас есть отличный ресурс ;)
                    по п2: развивайте проект! начало хорошее.
                    • 0
                      Re: по п2: очень ждал этих слов. спасибо, буду стараться.
                    • +3
                      ещё подумалось, что было бы классно так (текучий интерфейс?):
                      test.it(some).comment('my comment');
                      • +1
                        да. скорее всего следующий коммит будет это реализовывать.
                        Просто первоначально я думал что лучше возвращать true или false, что бы можно было делать конструкции типа
                        if (test.it(something)) { doSomething(); }
                        

                        но уже во время написания статьи понял что лучше возвращать test, дабы обеспечить цепочную связность
                        test.it(something).comment('my comment');
                        
                        именно по этому не описывал в статье идею с if (test.it())

                        Главное что будет обратная совместимость. Можно будет комментарии добавлять так, как указано в статье, и новым способом.
                        • +1
                          я бы тогда подумал над реализацией тернарного оператора, как то так (только как идея):
                          test.it(something, callback_if_true, callback_if_false [, callback_finally]).comment('comment for true', 'comment for false')
                          • +1
                            Мне очень не хочется уходить от
                            switch (arguments.length) {
                                /** in case of no arguments - throw Reference error */
                                case 0 : { ... } break;
                                /** if there only one argument - test it for truth */
                                case 1 : { ... } break;
                                /** if there are two arguments - test equalence between them */
                                case 2 : { ... } break;
                            
                            в test.it. возможно просто реализую это в отдельном методе, test.withCallback например.
                            Так или иначе идея хорошая, обязательно подумаю об этом.
                            • 0
                              Вполне вариант завести под это отдельную функцию. Правда я сторонник единого интерфейса в этом случае.
                              • +2
                                Пришла идея сделать callback так же как comment. Тогда будет возможно писать вот такие цепочики:
                                test.it(some)
                                    .callback(function(){ ... },function(){ ... })
                                    .comment('sometext');
                                

                                Надо подумать. может есть ещё какой функционал, которым можно будет обвешать тесты, подобным образом.
                                • 0
                                  Этот вариант мне нравится больше, но тогда уже лучше test.callback(...).it(...)? Чтобы внутри it можно было callback уже вызвать.
                                  • 0
                                    хм… можно подробнее?
                                    • 0
                                      Ну, делаем test.callback(колбек).it(some), чтобы на момент запуска .it(some) callback был уже установлен, и внутри it можно было бы его вызвать. Как вам такой вариант?
                                      • 0
                                        Вариант имеет право на существование, но я не понимаю чем он лучше
                                        test.it(some).callback(function(){ ... },function(){ ... })
                                        Тут коллбек выбирает функцию для исполнения, основываясь на результате теста.
                                        Это продолжает логику цепочности — когда начинает цепочку it(), а уже все остальные звенья отталкиваются от его результатов.
                                        Сегодня уже реализовал цепочки:
                                        // результат
                                        test.it('single').comment('comment').result(); // -> true
                                        test.it('first','second').comment('comment').result(); // -> false
                                        // аргументы
                                        test.it('single').comment('comment').arguments(); // -> 'single'
                                        test.it('first','second').comment('comment').arguments(); // -> ['first','second']
                                        

                                        Я не вижу смысла менять последовательность, если результат не измениться.
                                        Есть какая-то особенность вашего вариант, которую я не понимаю? В чём преимущество задавать коллбеки до, если функция в любом случае будет выбираться на основе прохождения теста?
                                        Серьёзно, если есть преимущество — скажите какое. Я как раз на днях буду реализовывать этот функционал, и хочется сделать обоснованный выбор между этими реализациями.
                        • +2
                          А так? .result() вернет значение true или false

                          if (test.it(something).comment('my comment').result()) {
                            //test passed
                          } else {
                            //test failed
                          }
                          


                          И ещё — .value() вернет переданное значение (удобно для отладки в цепочках, если передали 2 аргумента — вернуть массив)

                          var h = {a: {b: {r: '...'}}};
                          test.it(h.a.b).comment('hab?').value().r //should return '...'
                          
                          • +1
                            Отличные идеи! спасибо, обязательно реализую их.
                            • 0
                              Уже готово. сегодня запушу. Ещё раз спасибо за идеи.
                  • 0
                    Было бы замечательно, если бы в test.group(groupname, fun) можно бы было передавать контекст исполнения функции fun.
                    • +1
                      А чем bind не угодил?
                    • –3
                      Идея отличная, но вот что не нравится сразу:
                      1. Если вы переопределите функции console.*, вы не потеряете связь консоли с кодом и будет отображаться файл и номер строки где тест провален (а не номер строки в вашем скрипте)
                      2. Код был бы приятнее и удобнее, если бы выглядел как-то так:
                        var testObject = TestFactory->create(testName, parentGroupName | null);
                        var Me = {name:'Titulus',lastName:'Desiderio'};
                        testObject
                          ->test(Me, 'Я существую?')
                          ->test(OtherObject);
                        Me.habr = 'Хабрахабр!';
                        testObject->test(Me.habr, 'Хабр?');
                        

                        Это бы позволило в произвольном месте в коде делать конструкции типа
                        var testObject = TestFactory->getTest(testName);
                        testObject->test(Obj, 'Возможно здесь тест пройдет');
                        

                      • +1
                        Можно поподробнее о п.1? Меня как раз заботил вопрос, как бы выводить правильный trace()
                        По поводу п.2 — Боюсь что в ближайшем будущем я ни то что сделать, а просто понять такое не смогу.
                        • +1
                          п1. Попробуйте использовать window.onerror. Например перед запуском подменять его, выкидывать исключение если тест завален, в window.onerror вы увидите номер строки и файл.
                          Еще посмотрете на javascript-stacktrace.

                          Развивайте проект.
                          • 0
                            По первому пункту вот статья, а вот гитхаб.
                            Будут вопросы — пишите в личку, расскажу подробнее о нюансах «подмены консоли».
                          • 0
                            > testObject->test

                            Какой-то странный синтаксис
                            • +1
                              php
                              • +1
                                Баксов нет.
                                • 0
                                  я имел в виду исключительно "->" :)
                              • 0
                                Это опечатка головного мозга у меня.
                              • 0
                                Стоп, п2 это просто php код?
                                Я уж думал я пропустил какую-то весьма важную часть js
                                Если мне память не изменяет, то
                                testObject->test(Me.habr, 'Хабр?');
                                
                                это
                                testObject.test(Me.habr, 'Хабр?');
                                

                                Так зачем заставлять пользователя писать
                                testObject = TestFactory.create(testName, parentGroupName | null);
                                
                                если в библиотеке и так есть
                                window.test = new testit();
                                

                                идея в том что бы переименовать конструктор testit в TestFactory, метод it в test, отдать создание объекта test пользователю, и переписать всё на PHP?
                                • 0
                                  MuLLtiQ, titulusdesiderio: Отвечу всем сразу. Придрались как могли ) Очевидно же, что случайно в голове смешались два языка. Конечно, я имел ввиду нечто типа
                                  var testObject = TestFactory.create(testName, parentGroupName | null);
                                  var Me = {name:'Titulus',lastName:'Desiderio'};
                                  testObject
                                    .test(Me, 'Я существую?')
                                    .test(OtherObject);
                                  Me.habr = 'Хабрахабр!';
                                  testObject.test(Me.habr, 'Хабр?');
                                  

                                  идея в том что бы переименовать конструктор testit в TestFactory, метод it в test, отдать создание объекта test пользователю, и переписать всё на PHP?

                                  А теперь с комментариями попытаюсь объяснить идею:
                                  // TestFactory - глобальный объект, хранит в себе всю логику и все вызовы тестов
                                  var testGroup = TestFactory.create(groupName, parentGroupName | null); // Создает новую группу тестов. Она может быть корневой или дочерней
                                  // Далее идут тесты в группе
                                  // Тесты в эту группу могут попасть и из других участков кода при необходимости
                                  testGroup
                                    .test(Me, 'Я существую?')
                                    .test(OtherObject);
                                  testGroup.test(Me.habr, 'Хабр?');
                                  
                                  
                                  // Другой пример использования
                                  var testGroup = TestFactory.get(groupName); // Получаем группу по имени
                                  testObject.test(Obj, 'Возможно здесь тест пройдет'); // Добавляем тест в существующую группу
                                  

                                  Мне такой подход кажется более логичным и лаконичным
                                  • 0
                                    Мне нравится идея обращения к группе из любого места в коде. Но не так как описали вы.
                                    Думаю у меня получится реализовать это и без смены текущего синтаксиса
                                    test.group('groupName',function(){ ... }) // Первый вызов группы groupName
                                    test.it(...);
                                    test.group('groupName',function(){ ... }) // Второй вызов группы groupName
                                    
                                    То есть группы с одинаковыми groupName будут объединяться в одну.

                                    А в вашем подходе мне не нравятся 2 момента
                                    1. Синтаксический сахар, Теперь группу приходиться идентифицировать не только по groupName, но и по переменной, которой эта группа присвоена.
                                    2. Следующий код вылетит с ошибкой RequestError, которая не будет отловлена. Или я не понимаю как её отловить
                                      var testGroup = TestFactory.get(groupName);
                                      testObject.test(h.a.b.r, 'Возможно здесь тест пройдет');
                                      
                                    • 0
                                      Я ведь не предлагаю вам готовое решение, это лишь мое видение с первого взгляда.
                                      1. Вы не совсем верно поняли. Группу можно идентифицировать по имени группы в любом месте как-то так:
                                        var testGroup = TestFactory.get('core_tests');
                                        

                                        А дальше вы можете добавлять тесты в эту группу, вам не надо будет где-то в недрах выискивать группу по имени, вы получите полноценный объект группы, которым можно будет удобно оперировать. Как только переменная станет не нужна, забудьте о ней — группа и тесты сохранятся в глобальном TestFactory.
                                      2. Вообще там опять опечатка (сегодня что-то невнимательный я), не testObject, а testGroup.
                                        Можно сделать так:
                                        var testGroup = TestFactory.getOrCreate(groupName); // Будет получена существующая группа или создана новая корневая
                                        testGroup.test(h.a.b.r, 'Возможно здесь тест пройдет');
                                        

                                        А можно так:
                                        var testGroup = TestFactory.get(groupName);
                                        if (testGroup) {
                                          testGroup.test(h.a.b.r, 'Возможно здесь тест пройдет');
                                        }
                                        



                                      И еще по поводу синтаксиса:
                                      test.group('groupName',function(){ ... }) // Первый вызов группы groupName
                                      // Тут много много кода
                                      test.it(...); // Как понять и не запутаться, в какую группу попадет этот тест?
                                      
                                      • 0
                                        Как понять и не запутаться, в какую группу попадет этот тест?

                                        Обычно это решается табуляцией. так можно наехать и на стандартный синтаксис.
                                        function myFunction(){ ... }) // Вызов функции
                                        // Тут много много кода
                                        var myVar = 10; // Как понять и не запутаться, где находится переменная myVar, в области видимости функции или в глобальной.
                                        


                                        if (testGroup) {
                                          testGroup.test(h.a.b.r, 'Возможно здесь тест пройдет');
                                        }
                                        

                                        Точно так же выбросит ошибку и прервёт выполнение дальнейшего кода.
                                        • 0
                                          Это всё-равно синтаксический сахар, который может привести к печальным последствиям из-за невнимательности/неслаженности команды разработчиков:
                                          var testGroup = TestFactory.getOrCreate(groupName1);
                                          testGroup.test(some[0]);
                                          testGroup.test(some[1]);
                                          // код
                                          var testGroup = TestFactory.getOrCreate(groupName2); // переопределяем testGroup
                                          // ещё код
                                          testGroup.test(some[3]);
                                          
                                          В этой ситуации, если применять одну и ту же переменную, как буфферный testGroup, можно не заметить её переопределение и запутаться в тестах. А если использоватья каждый раз новую переменную с другим названием, то зачем разработчику придумывать два разных названия для идентификации одного и того же объекта? Есть и третий вариант, использовать разные названия, для разных переменных, но совпадающие с названиями групп
                                          var groupName1= TestFactory.getOrCreate('groupName1'); // определяем первую группу
                                          var groupName2= TestFactory.getOrCreate('groupName2'); // определяем вторую группу
                                          
                                          Но вот тут встаёт вопрос о сахаре — зачем нам в двух местах писать одно и то же?

                                          Если я вас не правильно понял, то поправьте пожалуйста. Я на самом деле не пытаюсь ругаться или защищать свою позицию любой ценой. Просто я не понимаю преимущества вашего подхода, а недостатки я описал выше
                                          • 0
                                            Вы наверное не работали с большими проектами на JS. Когда очень много файлов, классов, сотни объектов и сложная структура приложения, у вас не получится просто «взять и протестировать здесь и сейчас». С моим подходом вы сможете в разных местах вашего приложения (в разных файлах/объектах) добавлять тесты в одну и ту же группу.
                                            Представьте, что у вас есть приложение. Для примера, есть несколько файлов, в каждом файле описывается отдельный компонент, назовем условно: ядро, внешняя библиотека ядра, контроллер, шаблонизатор. Вы сможете создать группу теста в ядре, во внешней библиотеке добавить тесты в эту же группу, в контроллере создать группы теста контроллера и отображения, в шаблонизаторе добавить тесты в группу, созданную в контроллере и т.п.

                                            А о переопределении переменной вам подскажет IDE, точно также, как и об области видимости, в которой она объявлена.
                                • +2
                                  Кроме Mocha есть Jasmine, один из самых популярных фреймворков для тестирования JS кода. Да и вообще JS код тестировать начали не вчера. Хотя свои велосипеды в начале пути писать в целом полезно…
                                  • 0
                                    Тоже хотел про нее сказать, но почему-то был уверен, что она выходит исключительно в виде gem`а.
                                    Да и Konacha как-то интереснее имхо.
                                  • +1
                                    pivotal.github.io/jasmine/
                                    Есть вот такая продвинутая библиотека для тестирования js.
                                    Опередили)
                                    • 0
                                      ИМХО, получилось вполне хорошо.

                                      Оставлю сдесь тогда и свой велосипед юнит тесты для phantomjs
                                      • 0
                                        Вариант для Node.JS будете делать? :)
                                        • 0
                                          К сожалению никогда с Node.js не работал. Так что либо для начала мне надо разобраться с ним, либо с удовольствием приму pull-request.
                                          При условии что это вообще возможно конечно (:

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