31 июля 2013 в 13:31

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. Пост будет перенесён в хаб «Я Пиарюсь», как только наберу достаточно кармы.
Кунцевич Андрей @titulusdesiderio
карма
28,0
рейтинг 0,0
JS-dev | IT-specialist
Похожие публикации
Самое читаемое Разработка

Комментарии (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']
                          

                          Я не вижу смысла менять последовательность, если результат не измениться.
                          Есть какая-то особенность вашего вариант, которую я не понимаю? В чём преимущество задавать коллбеки до, если функция в любом случае будет выбираться на основе прохождения теста?
                          Серьёзно, если есть преимущество — скажите какое. Я как раз на днях буду реализовывать этот функционал, и хочется сделать обоснованный выбор между этими реализациями.
                          • 0
                            Ага, так тоже можно.
          • +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
                И вам спасибо.
  • 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.
      При условии что это вообще возможно конечно (:

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