TDD в JavaScript. Разработка приложения

    Всем привет. Данная статья посвящается методологии Разработки через тестирование (TDD) в применении c JavaScript.
    Напишу лишь вкратце о методологии TDD, более подробную информацию можно почерпнуть из ссылки выше.

    Разработка через тестирование подразумевает, что перед написанием кода необходимо:
    • создать тест, который будет тестировать несуществующий код, тест соответственно не пройдет
    • написать код и убедиться, что тест прошел
    • почистить код
    • повторить


    Разработка через тестирование

    Итак, ближе к теме.

    Технические средства


    Библиотека для тестирования: QUnit — простая в использовании, но обладающая всем необходимым функционалом, библиотека.

    Концепция объектов


    Разделим концептуально все объекты в системе на 3 категории:
    1. Базовые. Это классы, формирующие ядро системы, отвечающие за работу с хранилищами (к примеру GoogleGears, IndexedDb и пр.), взаимодействие между модулями, языковые пакеты, настройки пользователя и прочее
    2. Модули. Это классы, в которых реализуется основной функционал какого-либо модуля (к примеру, модуль для отображения истории звонков, модуль чата и пр.)
    3. Вспомогательные объекты. Объекты имеющие небольшой функционал и решающие конкретную небольшую задачу (к примеру, объект показывающий тултип, валидатор полей и пр.)

    Зачем разделять? От типа объекта будет зависеть полнота покрытия тестами его функционала. Базовые объекты — максимально, Модули — около 70%, Вспомогательные объекты — только для проектирования объекта (об этом подробнее чуть позже) и проверки результата.

    Именование объектов


    В данном случае используем наиболее простое наименование:
    Все объекты приложение будут начинаться со слова Application
    1. Базовые — Application_Core_ObjectName
    2. Модули — Application_Module_ModuleName
    3. Вспомогательные объекты — Application_Helper_ModuleName(если объект работает только с определенным модулем)_ObjectName

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

    Реализация объектов


    Т.к. мне нравится принцип «Простое лучше, чем сложное» (с), то реализация объектов будет максимально простой и (что очень важно!) совместимой с методологией TDD:

    Базовые и Модули
    function Application_Core_FuncName() {

      this.publicMethod = function() {
      }

      this._privateMethod = function() {
      }

    }


    * This source code was highlighted with Source Code Highlighter.



    Вспомогательные объекты
    var Application_Helper_HelperName = {
      init: function() {
      },

      publicMethod: function() {
      },

      _privateMethod: function() {
      }
    };


    * This source code was highlighted with Source Code Highlighter.


    Почему именно так? Начнем с постановки задачи: приложение будет разрабатываться по методологии TDD, т.е. необходимо реализовать доступ к приватным методам классов для тестирования (есть несколько мнений о необходимости тестировать приватные методы. Я тестирую). По общепринятым стандартам метод в JavaScript, начинающийся со знака подчеркивания, является приватным (многие IDE его не показывают в хинтах доступных методов), но реально этот метод доступен — ошибка не будет выброшена при доступе к, скажем, Application_Helper_HelperName._privateMethod().

    Разработка


    Допустим, нам нужно разработать некий вспомогательный модуль, который должен вернуть сгенерированный html с информацией о пользователе.

    Начнем с теста:

    module("User Info");

    test("main", function() {
      equals(typeof(Application_Helper_UserInfo), "object", "Check object");
    })


    * This source code was highlighted with Source Code Highlighter.


    Запускаем тест.

    Очевидно, что тест не проходит, т.к. такого объекта не существует.
    Создаем объект

    var Application_Helper_UserInfo = {

    };


    * This source code was highlighted with Source Code Highlighter.


    Тест прошел, переходим к следующему тесту.
    Данный объект принадлежит к типу Вспомогательные объекты, т.е. тесты будут писаться только для проверки результата и проектирования.
    Что такое проектирование при помощи методологии TDD? Это описание методов класса и их взаимодействия при помощи тестов.

    module("User Info");

    test("main", function() {
      equals(typeof(Application_Helper_UserInfo), "object", "Check object");

      equals(Application_Helper_UserInfo.hasOwnProperty("getHTML"), true, "Check existing method getHTML");

    });


    * This source code was highlighted with Source Code Highlighter.


    И реализация:
    var Application_Helper_UserInfo = {

      getHTML: function() {
      }

    };


    * This source code was highlighted with Source Code Highlighter.



    Далее по документации видно, что информация о пользователе состоит из: фотографии, имени и подробной информации. Поэтому спроектируем таким образом:

    module("User Info");

    test("main", function() {
      equals(typeof(Application_Helper_UserInfo), "object", "Check object");

      equals(Application_Helper_UserInfo.hasOwnProperty("getHTML"), true, "Check existing method getHTML");

      equals(Application_Helper_UserInfo.hasOwnProperty("_getPhoto"), true, "Check existing private method _getPhoto");

      equals(Application_Helper_UserInfo.hasOwnProperty("_getUsername"), true, "Check existing private method _getUsername");

      equals(Application_Helper_UserInfo.hasOwnProperty("_getInfo"), true, "Check existing method _getInfo");

    });


    * This source code was highlighted with Source Code Highlighter.


    Запускаем — новые тесты не прошли.


    Пишем реализацию.

    var Application_Helper_UserInfo = {

      getHTML: function() {
      },

      _getPhoto: function() {
      },

      _getUsername: function() {
      },

      _getInfo: function() {
      }
    };


    * This source code was highlighted with Source Code Highlighter.


    Теперь необходимо протестировать реализацию методов. Для тестирования мы создадим мок-объект (заглушку для тестирования) и проведем тестирование в его контексте.

    module("User Info");

    test("main", function() {
      equals(typeof(Application_Helper_UserInfo), "object", "Check object");

      equals(Application_Helper_UserInfo.hasOwnProperty("getHTML"), true, "Check existing method getHTML");

      equals(Application_Helper_UserInfo.hasOwnProperty("_getPhoto"), true, "Check existing private method _getPhoto");

      equals(Application_Helper_UserInfo.hasOwnProperty("_getUsername"), true, "Check existing private method _getUsername");

      equals(Application_Helper_UserInfo.hasOwnProperty("_getInfo"), true, "Check existing method _getInfo");

      var mockUserInfo = {
        username: 'Name',
        photo: 'photo.png',
        info: 'Information'
      };

      var photo = Application_Helper_UserInfo._getPhoto.call(mockUserInfo);
      
      equals(photo, 'photo.png', 'Checking photo');

      var username = Application_Helper_UserInfo._getUsername.call(mockUserInfo);
      
      equals(username, 'Name', 'Checking username');

      var info = Application_Helper_UserInfo._getInfo.call(mockUserInfo);
      
      equals(info, 'Information', 'Checking information');
    });


    * This source code was highlighted with Source Code Highlighter.



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

    var Application_Helper_UserInfo = {

      getHTML: function() {
      },

      _getPhoto: function() {
        
        return this.photo;
      },

      _getUsername: function() {

        return this.username;
      },

      _getInfo: function() {

        return this.info;
      }
    };


    * This source code was highlighted with Source Code Highlighter.


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

    module("User Info");

    test("main", function() {
      equals(typeof(Application_Helper_UserInfo), "object", "Check object");

      equals(Application_Helper_UserInfo.hasOwnProperty("getHTML"), true, "Check existing method getHTML");

      equals(Application_Helper_UserInfo.hasOwnProperty("_getPhoto"), true, "Check existing private method _getPhoto");

      equals(Application_Helper_UserInfo.hasOwnProperty("_getUsername"), true, "Check existing private method _getUsername");

      equals(Application_Helper_UserInfo.hasOwnProperty("_getInfo"), true, "Check existing method _getInfo");

      var mockUserInfo = {
        username: 'Name',
        photo: 'photo.png',
        info: 'Information'
      };

      var photo = Application_Helper_UserInfo._getPhoto.call(mockUserInfo);
      
      equals(photo, 'photo.png', 'Checking photo');

      var username = Application_Helper_UserInfo._getUsername.call(mockUserInfo);
      
      equals(username, 'Name', 'Checking username');

      var info = Application_Helper_UserInfo._getInfo.call(mockUserInfo);
      
      equals(info, 'Information', 'Checking information');

      var html = Application_Helper_UserInfo.getHTML.call(mockUserInfo);

      if (html != undefined && html.indexOf('id="application_helper_userinfo"') != -1 && html.indexOf('Name') != -1 && html.indexOf('photo.png') != -1 && html.indexOf('Information') != -1) {

        ok(true, "HTML ok");

      } else {
        ok(false, "HTML does not pass");
      }
        
    });


    * This source code was highlighted with Source Code Highlighter.


    Вот и все. Более подробные проверки на несуществующие параметры и прочее выполняться не будут, т.к. объект является вспомогательным, для базовых классов или модулей необходимо было бы написать дополнительные тесты, проверяющие состояние, повторные вызовы, работу с DOM-деревом и прочее.

    Теперь необходимо написать окончательную реализацию объекта, ведь тест, который был написан, не прошел.

    var Application_Helper_UserInfo = {

      getHTML: function() {
        var html = '<div id="application_helper_userinfo">';
        html += '<div>' + Application_Helper_UserInfo._getPhoto.call(this) + '</div>';
        html += '<div>' + Application_Helper_UserInfo._getUsername.call(this) + '</div>';
        html += '<div>' + Application_Helper_UserInfo._getInfo.call(this) + '</div>';
        html += '</div>';
        return html;
      },

      _getPhoto: function() {

        return this.photo;
      },

      _getUsername: function() {

        return this.username;
      },

      _getInfo: function() {

        return this.info;
      }
    };


    * This source code was highlighted with Source Code Highlighter.





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

    Использование методологии TDD увеличивает время разработки приблизительно на 40%, но сокращает время багфиксинга в разы. Поддерживать и развивать проект становится намного проще, он становится более стабильным и последовательным, а QA остается лишь тщательно протестировать интерфейс. Буквально сегодня проводилось тестирование двух модулей моего проекта — один с использованием TDD, второй — без. Итог говорит в пользу TDD — в первом модуле QA нашли лишь небольшие проблемы с графикой в IE9, зато во втором — неприятный баг, который 100% обнаружился бы при разработке через тестирование.

    Так же актуальная проблема разработчиков — отсутствие предварительного проектирования. Если задача кажется простой, то многие занимаются сразу же реализацией, считая проектирование пустой тратой времени. Проектирование — это прекрасно. Множества ситуаций, когда стыдно показать свой конечный код, можно было бы избежать при помощи предварительного проектирования.

    На этом, пожалуй, закончу. Статья получилась больше, чем я рассчитывал, но освещена лишь малая часть TDD в JavaScript.
    Если будут желающие, то могу продолжить про TDD и Ajax, тестирование сложных модулей, асинхронное тестирование состояний, автоматически генерируемые мок-объекты и фреймверки для этого.

    Спасибо за внимание. До новых встреч.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 26
    • +2
      > Итог говорит в пользу TDD — в первом модуле QA нашли лишь небольшие проблемы с графикой в IE9, зато во втором — неприятный баг

      Итог: 1 баг при TDD и 1 баг при обычной разработке, при этом времени затрачено было на 40% ,jkmit/
      Что-то не так? :)

      Проектирование != TDD, проектирование можно делать в удобных средах, и даже на бумажке, строя вменяемую архитектуру, без всякого TDD.

      Я предпочитаю сначала написать код, а потом накладывать на него юнит-тесты, дабы следующие версии ничего не ломали. А не наоборот.
      Хотя наверно, это дело вкуса.
      • +1
        Баг в ИЕ9 был скорее не багом — на 10 пикселей ниже чем нужны было завершение окна. И даже если бы QA не нашли — приложение работало бы корректно, скорее всего пользователи не узнали бы о том, что баг существует. Ну разве что сравнили с другим браузером.

        Я же писал — проектирование при помощи TDD. Все дело привычки и личных предпочтений — на практике для моего проекта методология TDD показала потрясающие результаты.
        • +3
          Все-таки, тесты проще писать сразу, просто нужно привыкнуть.
          • +2
            писать тесты после — все равно что не писать вовсе
          • 0
            > «Использование методологии TDD увеличивает время разработки приблизительно на 40%, но сокращает время багфиксинга в разы.»

            Откуда эти цифры?
          • +3
            У вас больше половины тестов проверяют, что есть такой-то объект, и у него есть методы с такими-то названиями. Это реально так полезно и спасает от кучи багов потом?
            • 0
              Да, и правильнее будет «check existence of».
              • 0
                Тесты в основном пишутся не для того, чтобы проверить «сейчас», а для того чтобы ничего не сломать «потом».
                • +2
                  Для динамических интерпретируемых языков полезно, если хочется получить внятное сообщение о том, что метода не существует, а не эксепшен в тесте на то что метод возвращает.
                  • +1
                    Эта часть тестов является проектированием через тестирование.
                    Код непосредственного тестирования результатов находится после проектирования. Ведь логично сперва разработать структуру, а лишь затем — реализацию. Об этом я как раз и писал.

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

                    Я не стал перегружать излишними проверками, т.к. лишь показал общий принцип. В реальном объекте тесты проектирования занимают менее 10%
                  • 0
                    Было бы интересно прочитать про инструментарий и вообще организацию тестирования клиентского JavaScript. На сервер-сайде применяю давно, а вот как тестировать на клиенте то, что сервер выдал не очень представляю. Посмотрел QUnit, но всё равно как-то не понял как тестировать. Вот есть страница, есть JS, который работает с её DOM в том числе через AJAX. Куда тесты вставлять, как проверять что функция корректно DOM изменила?
                    • 0
                      P.S. Картинку сами рисовали? Как-то странно выглядит — как минимум нет тестов после того как почистили код.
                      • 0
                        Картинка взята с википедии. При дальнейшей разработке мы выполняем все тесты и в любом случае видим, если что-то сломалось.
                        • 0
                          Если следовать картинке, то получим, что в какой-то момент времени у нас может быть несколько несработавших тестов, но не срабатывают они по разным причинам.
                      • +2
                        Это очень интересная (лично для меня) тема и она достойна отдельной статьи. Как только я найду чуть больше времени я обязательно напишу про асинхронное Ajax тестирвоание
                        • 0
                          Буду ждать. Заранее спасибо.
                      • +1
                        Советую так же посмотреть на Jasmine фреймврок. Тоже достаточно удобно.
                        • 0
                          Спасибо, посмотрю
                          • 0
                            С qunit не сравнить, он в разы круче
                            • +1
                              *jasmine круче
                              • 0
                                Я сначала не понял, про что вы, хотел уже написать комментарий в защиту :)
                                • 0
                                  Да я и сам не понял ага :D
                          • 0
                            У qUnit, на мой взгляд, есть недостаток — он требует ручного запуска/перезапуска браузера, что надоедает. Чтобы не обращаться к браузеру и получить TEST PASSED/TEST FAILED из коммандной строки или автоматического сервера интеграции типа Hudson, то рекомендую jstestdriver
                            • 0
                              Лично мне статья пмогла понять что же такое TDD вообще. Спасибо.
                              >Если будут желающие, то могу продолжить про TDD и Ajax, тестирование сложных модулей, асинхронное тестирование >состояний, автоматически генерируемые мок-объекты и фреймверки для этого.

                              Есть, есть желающие!

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