12 октября 2014 в 12:54

Unit тестирование в js. YATS — поделка для написания юнит тестов

Добрый день!

Начать свою статью я хочу с небольшого вступления. Вы помните, какими были сайты лет 10 назад? а пять? Если сайты и содержали какую-то js логику, то она была проста и наивна. На сегодняшний день каждый второй — это не просто статические данные, это большое динамическое содержимое, с «кучей» js логики.

За 5-8 лет JavaScript перестал быть языком для анимирования снежинок под новый год и преобразовался в довольно популярный и востребованный язык программирования, с большим коммьюнити.

Любой код можно сделать лучше, если покрыть его тестами. Код, покрытый тестами проще рефакторить, при написании tests first можно писать удобный расширяемый код.

В таких задачах хорошо помогает UNIT-тестирование.

На сегодняшний день существует множество фреймворков для unit тестирования js кода. В данной статье я бы хотел описать свое видение небольшой библиотеки для тестирования js кода.


Зачем хотим



Создать небольшую библиотеку, содержащая необходимые методы для работы с кодом и предоставляющая удобный интерфейс для работы как на клиентской стороне, так и с node.js

Что хотим



  • Поддержка цепочных вызовов для возможности писать код без постоянного «упоминания» объекта для тестирования
  • Не засорять кучей глобальных переменных пространство (при подключении напрямую)
  • Наличие как общих методов для тестирования, так и специфичных, но подчас необходимых (рекурсивное сравнение объектов и т.п.)
  • Поддержка подключения как «нативно» (через тег script), так и с помощью require.js (либо commonJS)
  • Возможность получить результаты тестов как консольно (клиентская сторона\node.js), так и в виде html (мобильные браузеры)
  • Возможность выбрать html ноду в качестве наблюдаемой и очищать после каждой группы тестов (удобно для тестирования, например, виджетов)
  • Возможность группировать тесты и получать сложные деревья тестов, например:
    Тесты модуля N
    |
    |__Тесты подмодуля N:X
    | |
    | |__Тесты функции Z подмодуля X
    |
    |___Тесты функции Y модуля N


Что получилось



Чтобы долго не томить рассказами — библиотека находится здесь: github.com/xnimorz/YATS (здесь же и небольшое количество примеров)
wiki страница — xnim.ru/blog?id=38 (здесь ждет более подробное описание возможностей библиотеки с примерами)
Посмотреть работу библиотеки можно здесь: xnimorz.github.io/YATS/EXAMPLE.html
Пример кода для тестирования — браузеры ( github.com/xnimorz/YATS/blob/master/example.js ) — здесь содержится пример работы всех методов бибилиотеки
node.js — github.com/xnimorz/YATS/blob/master/nodeExample.js

Что умеем



Библиотека создает глобальный объект yats (если подключать не через require).
Основными методами библиотеки являются методы:
  • ok — проверяет выражение на истинность (принимает любые объекты, в том числе функцию, которую запускает на выполнение)
  • not — аналогично ok, но проверяет на ложность
  • group — позволяет группировать тесты в отдельные группы. Имеется возможность создавать любую иерархию групп.
  • groupClose — закрывает последнюю активную группу. (при вызове несколько раз подряд — будет последовательно закрывать активные группы)
  • comment — описывает комментарий к тесту
  • getHtmlResult — получает html результатов тестов
  • toConsole — выводит результаты тестов на консоль


Используя данные методы можно приступать к написанию тестов с помощью данной библиотеки.
Остальные методы можно почерпнуть из wiki страницы, либо из README на гитхабе (так как они являются более специфичными и позволяют писать тесты более гибко, но не влияют на остальную работу библиотеки).

Описание методов


comment({string}) — добавляет комментарий к следующему тесту.

getHtmlResult — предоставляет результаты тестов в виде html

toConsole — выводит результаты тестов в консоль

ok({object}) — принимает переменную\функцию\логическое выражение. В случае с функцией вызывает ее и обрабатывает результат выполнения).
Аналогично работает функция not
пример работы:

yats
    .comment('Это пример комментария').ok(1 === 1)
    .comment('Тестирование существования объекта').ok({someObjectField: 23})
    .comment('функция-аргумент метода ok будет вызвана').ok(function(){return true;})
    .comment('Это последний комментарий').not(function(){return false;})
    .comment('Это пример работы, когда тест не проходит').ok()
    .comment('Это пример работы, когда функция вызывает исключение').ok(function() {throw 'some exception';})
    .toConsole();




Успешно пройденные тесты помечаются синим цветом. Не прошедшие тесты или тесты, в которых был сгенерирован exception — оттенками красного.

group({string} название группы,{string} описание группы, {function} функция) — основной параметр — название группы. Помечает новую группу заданным названием. При необходимости можно задать дополнительное описание группы. (необязательно)
Также, функция, которая описывает группу тестов необязательна. Если такая группа определена, то все тесты внутри данной функции будут относится к выбранной группы (допустимы вложенности групп). По окончании функции, группа будет закрыта.
Если такую функцию не определять, то группу необходимо закрыть вручную с помощью функции groupClose().

Например, приведенные тесты идентичны:

yats
    // #1
    .group('ok/not')
        .comment('Пример теста внутри группы').ok(true)
    .groupClose()
    .toConsole()


    //Очищение стека тестов
    .clearStack()
    
    // #2
    .group('ok/not', function() {
            yats.comment('Пример теста внутри группы').ok(true)
        }
    )
    .toConsole();




Заметим, каждая подгруппа ведет свою статистику пройденных, заваленных тестов. Это позволяет быстро определить проблемный модуль.

Работа с группами:



Работа с группами выглядит следующим образом:
Разработчик получает возможность в файле описать работу каждого модуля, выделяя тесты для каждой функции в отдельную группу, например, у нас существует модуль cat:
var cat = {
    meow: function() {
        this.say = 'meow';
        this.action = 'say';
    },

    purr: function() {
        this.say = 'urrr';
        this.laught = true;
        this.action = 'say';
    },

    run: function() {
        this.action = 'run';
        this.say = '';
    }
};


Для тестирования данного модуля заведем группу тестов Cat:

yats.group('cat', function() {
// Код тестов
});


Внутри данной группы тестов обернем в тестирование каждой функции в отдельную группу тестов:

yats.group('cat', function() {

    cat.meow();
    yats
        .group('meow')
        .comment('что кот делает').ok(cat.action === 'say')
        .comment('что кот произносит').ok(cat.say === 'meow')
        .groupClose();

    cat.purr();
    yats
        .group('purr')
        .comment('что кот делает').ok(cat.action === 'say')
        .comment('что кот произносит').ok(cat.say === 'urrr')
        .comment('доволен ли кот').ok(cat.laught)
        .groupClose();

    cat.run();
    yats
        .group('run')
        .comment('что кот делает').ok(cat.action === 'run')
        .comment('что кот произносит').ok(cat.say === '')
        .groupClose();

});




Да, конечно, для данного примера необходимость создавать группу тестов для каждой функции притянута «за уши», но в случае, если для функции необхоидимо написать 3-5 и более тестов — перспектива обернуть эти тесты в группу кажется очень привлекательной. Мы получим более структурированный код и понятное отображение результатов теста.

Работа с DOM элементом.


YATS поддерживает «слежение» за DOM элементом. Для этого нам необходимо установить «рабочую ноду» и затем после каждой группы тестов данная нода будет очищаться.
Функции для работы с DOM элементом:

  • setWorkingNode({cssSelector}) — устанавливает рабочую ноду
  • resetWorkingNode() — сбрасывает рабочую ноду (более слежение за этим элементом не будет)
  • getWorkingNode() — получает рабочую ноды


Пример работы:

Предположим, что в html документе находится следующий код:
<div class="test-node">тест</div>


Код тестов:
yats
    .setWorkingNode('.test-node')
    .group('Пример работы с нодой', function() {

        yats
            .comment('Первая попытка получить innerHTML').ok(yats.getWorkingNode().innerHTML === 'тест')
            .comment('Внутри группы тестов рабочая нода не очищается').ok(yats.getWorkingNode().innerHTML === 'тест');

    })
    .toConsole()
    .group('Пример работы с нодой', function() {
        
        yats.comment('Получить не удается, так как данные в ноде уже удалены').not(yats.getWorkingNode().innerHTML);
        
    })
    .toConsole();




Работа с nodejs и браузерной консолью



В браузере группы тестов скрываются, чтобы предоставить пользователю общую информацию о тесте. Посмотреть необходимые результаты можно щелкнув по заданной группе тестов:


(Подробное описание тестов хранится за фразой Tests)

Для node.js группы тестов и вложенности определены табуляцией, что позволяет упростить «навигацию» в просмотре результатов тестов:



Таким образом


В данной статье была описана необходимая информация для быстрого старта с помощью данной библиотеки.
Информацию о дополнительных методах (проверка переданных аргументов на эквивалентность (equal), работа с фиксированием переменной (test) и т.д.) можно найти в страничке-описании и в readme к проекту github.com/xnimorz/YATS
Никита Мостовой @xnim
карма
26,0
рейтинг 0,0
Самое читаемое Разработка

Комментарии (27)

  • 0
    У меня тоже есть свой велосипед, так что несколько каверзных вопросов:
    1. Что с поддержкой асинхронных тестов?
    2. Что с возможностью останавливать дебаггер в месте падения?
    3. Как отключить вывод в отчёте информации о пройденных тестах? Это бесполезная информация, которая захламляет отчёт.
    4. Как быстро найти код упавшего теста? Желательно сразу в IDE.
    5. Как посмотреть стектрейс упавшего теста?
    • 0
      1. Поддержки асинхронных тестов на данный момент корректной нет. Можно использовать асинхронный запрос и тестирование результата в методе библиотеки.
      В будущем думаю добавить возможность тестирования асинхронных запросов с помощью promises

      2. Не задумывался на данной возможностью, но реализовать такую поддержку можно добавив дополнительный параметр в конфигурацию. Спасибо =)

      3. Информация о пройденных тестах на данный момент не отключается

      4. Код упавшего теста можно найти, например по его описанию + группе

      5. Аналогично не занимался данной проблемой. При необходимости можно немного модифицировать логику работы библиотеки следующим образом —
      Здесь github.com/xnimorz/YATS/blob/master/yats.js#L120 добавляем создание переменной Error и забираем его stackTrace — При необходимости можем удалить ненужные вызовы и представить для пользования корректный stackTrace.
      • 0
        1. Не очень понятно как тут помогут promises.

        2. Типа как в QUnit, где отключаются все try-catch и все тесты валятся после первого же исключения? Лучше запускать каждый тест отдельным событием — тогда падение теста не будет сказываться на общем цикле запуска тестов. Пример, такого запуска можно глянуть тут

        3. Группировку тоже было бы не плохо иметь возможность отключать. Потому что прокликивать группы после каждого падения тестов — не очень удобно.

        4. Было бы куда удобнее просто кликнуть по гиперссылке:
        image

        5. Многие тестовые фреймворки используют отдельные библиотеки ассертов, которые просто кидают исключение. Это позволяет использовать единый код для обработки падения теста — ведь не важно, возникло ли исключение из-за вызова несуществующего метода или из-за того, что значение возвращённого ею результата не равно ожидаемому.
        • 0
          1. — Как можно решить задачу с использованием promises — Перед запуском теста создаем объект промиса и запускаем таймер (нам нужно знать — может быть наш запрос отвалится по таймауту). И далее терпеливо ждем, что произойдет далее — либо асинхронный запрос завершится и мы резолвим промис, попутно проверяя данные с которыми произошел резолв (собираем данные теста)
          Либо падаем по reject по таймауту.

          2. — да, можно подумать в этом направлении

          3. — думаю, стоит создать единый конфиг для тестов, куда включить — — отображение стектрейса для падения теста или exception
          — скрывать пройденные тесты (соответсвенно, если группа прошла — не показываем группу)

          4 — картинка не отобразилась =( Но это можно решить, воспользовавшись выводом со стектрейса (например выводить первый элемент, не относящийся к библиотеки)

          5 — мне кажется, что стоит различать два варианта развития событий — 1) падение теста, так как вернулось неожиданное значение
          2) внештатное поведение кода, которое привело к исключению
          • 0
            1. А, понятно. Ну в принципе тут хватило бы и простого метода done()

            3. Не очень понимаю за чем стектрейс может потребоваться скрывать. Ну и зачем показывать пройденные тесты — тоже.

            4. Извиняюсь, вот правильные ссылки:



            5. А зачем?
            • 0
              1. Метода done() хватило бы, но promises несут большую гибкость

              3. По идее, да, стектрейс желателен бы всегда. Согласен

              4. О чем, в принципе я и говорил. Для большей гибкости вывод теста при запуске в node.js можно сохранять в файл -> получаем доступ к файлам одним кликом. Для работы с DOM в node.js можно прикрутить phantomjs, но это уже вопрос не тестовой системы

              5. Мне все таки кажется, что падение с Exception !== падение, если функция вернула неверные данные.
              В первом случае — это внештатная ситуация, во второй — неверный расчет
              • 0
                5. Функция вернула не тот тип параметра из-за чего у него не оказалось нужного метода — это «неверный расчёт» или «внештатная ситуация»?
                • 0
                  Изначально мы видим, что происходит внештатная ситуация. И только затем определяем, что внештатная ситуация произошла по вине некорректного расчета (то есть мы раскручиваем «воронку» с начала)

                  И да, добавил вывод stackTrace. Спасибо
                  • +2
                    И тут мы приходим к вопросу: если различить эти два понятия может лишь программист в процессе «раскрутки», то зачем про эти различия знать тестовому фреймворку? Его задача — зафиксировать падение теста и выдать исчерпывающую информацию о нём.
                    • 0
                      да, соглашусь с Вами
  • 0
    Причем тут node.js?
    • 0
      Поддержка библиотеки полноценного запуска на node.js. Существование форматированного вывода, адаптированного под node.js
      • 0
        Зачем в node.js синхронный TDD?
  • +1
    Чем Mocha (читается "мокка") не устроил? Какие преимущества у сабжа перед ним?
    • 0
      Если оценивать по возможностям и функциональности — ничем.

      Для чего была создана данная библиотека?

      До создания этой библиотеки я работал с QUnit и Jasmine.

      Для своих задач было решено создать небольшую библиотеку, которая будет организована интерфейсно и возможностями под текущие задачи. Так и родилась затея с данной библиотекой.
      В нее были включены небольшие фичи, которых мне не хватало в QUnit (отдаю должное разработчикам — это прекрасный фреймворк для тестирования).

      К ним можно отнести вывод результатов, создание иерархии тестов, цепочные вызовы.

      Mocha для проектов никогда не применял, но знаком с их документацией.

      • 0
        Ну не скажите, сейчас mocha умеет гораздо больше чем ваша библиотека. Н-р минимум: вывод стека, разные репортеры. Все эти финтефлющечки и подсветки, на практике, нафиг не нужны.
        • 0
          Только вывод стека без гиперссылок — сомнительное удовольствие. И разные репортеры — это именно, что финтифлюшечки.
          • 0
            Не сказал бы что разные репортеры это финтефлюшечки. Разные репортеры позволяют адаптировать mocha к всему (есть вывод в xml для jenkins, отдельный вывод для консоли, отдельно для браузера, отдельно html вывод и тп — sauce labs, coveralls). Вы пробовали прикручивать ci к node.js проекту?
            • 0
              Тут не нужна куча репортов — достаточно одного-двух. В моче же это именно что финтифлюшки: visionmedia.github.io/mocha/#reporters
              • 0
                Давайте наоборот =), назовите те репортеры которые вы считаете нужны. Мне вот лично нужно и что я использую: xUnit, spec (чуть переделанный), html
                • 0
                  Адаптер к Karma и больше ничего не надо.
                  • 0
                    Как мне карма поможет, если у меня нет машин со всеми поддерживаемыми браузерами?
                    • –1
                      Использовать виртуалки.
                      • 0
                        Не всегда это работает, проблем появляется больше чем решается. Возьмите любой OS проект и не каждый автор будет тратится на отдельные лицензии для винды, macosx не особо весел живет в virtualbox, запуск мобильных браузеров вообще отдельная песня.
                        У вас есть живые примеры когда хватает одной кармы (естественно поддерживаются все распространненые браузеры)?
                        • 0
                          И каким образом различные репорты спасут вас от всех этих проблем?
      • 0
        Что-то мне кажется, что и про Karma вы ничего не слышали.

        Полагаю, что если бы вы вовремя попробовали Mocha+Chai+Karma, то писать велосипед бы не пришлось.
  • 0
    mocha+chai/jasmine = успех;

    Я считаю, что эти библиотеки полностью покрывают ваши потребности, новым людям на проекте будет проще читать их документацию, к тому же они имеют хороший задел в функциональности на будущее.

    В общем, мне кажется это тот случай, когда велосипед — во зло.

    Оче хорошо что вы написали его сами написали и поняли принцип работы фреймворков для тестирования изнутри, но не стоит призывать его использовать, вдруг кто то начнет :)

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