Pull to refresh

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

Reading time 9 min
Views 14K
Всем привет. Данная статья посвящается методологии Разработки через тестирование (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, тестирование сложных модулей, асинхронное тестирование состояний, автоматически генерируемые мок-объекты и фреймверки для этого.

Спасибо за внимание. До новых встреч.
Tags:
Hubs:
+42
Comments 26
Comments Comments 26

Articles