Статьи

Модульное тестирование приложений Backbone.js

Потратив часы, а то и дни на то , чтобы завершить работу над удивительной новой функцией для вашего веб-приложения, вы, наконец, готовы увидеть ее в действии. Вы добавляете новый код в базу JavaScript, создаете кандидат на выпуск и запускаете браузер, ожидая, что вы будете поражены. Тогда … Э-э-э … новая функция может работать нормально, но какая-то другая важная часть вашего приложения — часть, которую вы не трогали при разработке новой версии — ужасно ошиблась. Теперь вы сталкиваетесь с проблемой возврата в течение нескольких дней работы, чтобы попытаться выяснить, как вы нарушили существующий код. Счастливые дни определенно не здесь снова.

Именно этот сценарий укусил меня больше, чем я хотел бы признать. И если вы некоторое время программировали, вы, вероятно, тоже это видели. Рассмотрим, однако, что делает этот сценарий таким болезненным. Это не совсем так, потому что наш новый код сломал существующий код; это неизбежно в развитии. Настоящая боль в том, что потребовалось так много времени, чтобы заметить поломку. С таким большим количеством разработок, так как мы знали, что наше приложение работало, существует огромное количество кода, в котором может скрываться ошибка. И хотя это может показаться чем-то вроде охоты на иголку в стоге сена, у нас нет другого выбора, кроме как погрузиться в нее.

В этой статье мы действительно собираемся исключить этот сценарий из нашей разработки JavaScript. Больше не нужно копаться в часах, днях или неделях кода в поисках иглы. Принцип, который мы примем, прост: найдите любую ошибку, как только мы ее создадим. Это верно; мы собираемся настроить среду разработки и процесс, который немедленно сообщает нам, когда мы пишем код, который вводит ошибку. Кроме того, дополнительные усилия, которые мы прилагаем к этому процессу, не пропадут впустую, когда начальная разработка будет завершена. Тот же самый тестовый код, который фиксирует наши ошибки разработки, будет полностью использован в среде интеграции. Мы можем легко включить тесты в нашу систему управления исходным кодом, блокируя ошибки до того, как они попадут в нашу базу кода.

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

Сборка среды тестирования JavaScript

Наше модульное тестирование нирваны требует некоторых инструментов разработки, которых может не быть в вашем рабочем месте (пока). Хорошие и плохие новости заключаются в том, что существует множество вариантов. Это хорошая новость, потому что она дает нам варианты, и это плохие новости, потому что темпы развития внешнего интерфейса сегодня означают, что вариантов слишком много. Чтобы сфокусировать нашу оценку, давайте прямо скажем о двух наших главных целях. Все остальное вторично:

  1. Наша среда должна поддерживать непрерывное тестирование во время разработки без трения.
  2. Тесты, созданные во время разработки, должны одинаково использоваться в интеграции.

Среды исполнения

Для кодирования JavaScript нет лучшей среды разработки, чем современный веб-браузер. Независимо от вашего вкуса — Firebug или Webkit Developer Tools, браузер поддерживает проверку и редактирование DOM в реальном времени, полную интерактивную отладку и сложный анализ производительности. Веб-браузеры отлично подходят для разработки, поэтому наши инструменты тестирования и среда должны интегрироваться с разработкой в ​​браузере. Однако веб-браузеры не так хороши для тестирования интеграции. Интеграционное тестирование часто происходит на серверах где-то в облаке (или, по крайней мере, где-то в центре обработки данных). Эти системы даже не имеют графического пользовательского интерфейса, тем более современного веб-браузера. Для эффективного тестирования интеграции нам нужны простые сценарии командной строки и поддерживающая их среда исполнения JavaScript. Для этих требований предпочтительным инструментом является node.js. Хотя существуют и другие среды JavaScript командной строки, ни одна из них не имеет широты и глубины поддержки для сопоставления node.js. На этапе интеграции наши инструменты тестирования должны интегрироваться с node.js.

Тестовая структура

Теперь, когда мы установили, что наши инструменты тестирования должны поддерживать среды как веб-браузера, так и node.js, мы можем сузить выбор, достаточный для выбора базовой инфраструктуры тестирования. Существует множество тестовых сред JavaScript, но большинство из них сильно склонны к тестированию браузеров; заставить их работать с node.js, как правило, возможно, но часто требует нелегких взломов или настроек. Одной из структур, которая не страдает от этой проблемы, является Mocha , который оправданно описывает себя как:

Mocha — это многофункциональная среда тестирования JavaScript, работающая на узле и в браузере, что делает асинхронное тестирование простым и увлекательным.

Первоначально разработанный для node.js, Mocha был расширен для поддержки веб-браузеров. Используя Mocha в качестве нашей тестовой среды, мы можем писать тесты, которые поддерживают как разработку, так и интеграцию без изменений.

Библиотека утверждений

В отличие от некоторых тестовых сред JavaScript, Mocha был разработан для максимальной гибкости. Как следствие, нам придется выбрать несколько дополнительных частей, чтобы завершить его. В частности, нам нужна библиотека утверждений JavaScript. Для этого мы будем полагаться на библиотеку утверждений Чай . Chai несколько уникален тем, что поддерживает все распространенные стили утверждений — утверждай , ожидай и должен. Стили утверждений определяют, как мы пишем тесты в нашем тестовом коде. Под одеялом все они эквивалентны; легко перевести тесты из одного стиля утверждения в другой. Основное различие в стилях утверждений заключается в их удобочитаемости. Выбор стиля утверждения зависит главным образом от того, какой стиль вы (или ваша команда) считаете наиболее читаемым, и какой стиль создает наиболее понятные тесты. Чтобы увидеть разницу, подумайте о разработке тривиального теста для следующего кода:

var sum = 2 + 2; 

Традиционный тест в стиле assert можно записать так:

 assert.equal(sum, 4, "sum should equal 4"); 

Этот тест выполняет свою работу, но если вы не привыкли к тестированию в старом классе, вам, вероятно, будет сложно читать и интерпретировать. Альтернативный стиль утверждения использует expect :

 expect(sum).to.equal(4); 

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

 sum.should.equal(4); 

Библиотека Chai поддерживает все три стиля утверждений. В этой статье мы будем придерживаться should .

Шпионы, окурки и издевательства

Большинство веб-приложений, включая простой пример, который мы рассмотрим в этой статье, используют сторонние библиотеки и сервисы. Во многих случаях тестирование нашего кода потребует наблюдения — или даже контроля — этих библиотек и сервисов. Библиотека Sinon.JS предоставляет множество инструментов для тестирования этих взаимодействий. Такие инструменты делятся на три основных класса:

  • Шпион Тестовый код, который наблюдает за вызовами функций вне тестируемого кода. Шпионы не мешают работе этих внешних функций; они просто записывают вызов и возвращаемое значение.
  • Заглушка Тестовый код, который заменяет вызовы функций вне тестируемого кода. Код заглушки не пытается повторить внешнюю функцию; он просто предотвращает неразрешенные ошибки, когда тестируемый код обращается к внешней функции.
  • Издеваться Тестовый код, имитирующий функции или сервисы вне тестируемого кода. С помощью mocks тестовый код может указывать возвращаемые значения из этих функций или сервисов, чтобы он мог проверить ответ кода.

Наряду с самой библиотекой Sinon.JS мы можем дополнить стандартную библиотеку утверждений Chai с помощью Sinon.JS Assertions for Chai .

Среда разработки модульных тестов

Последний инструмент для нашего инструмента тестирования — это среда разработки для модульного тестирования. Для нашего примера мы будем использовать Test’em . Test’em — это набор удобных скриптов для настройки и запуска среды непрерывного тестирования. Мы можем, если захотим, сами написать сценарии и управлять средой вручную; однако Тоби Хо (создатель Test’em) собрал потрясающий пакет, который может избавить нас от неприятностей.

Пример приложения

Чтобы увидеть нашу среду тестирования в действии, давайте рассмотрим простое приложение. Несмотря на то, что это приложение соответствует базовым требованиям, оно включает в себя все функции, необходимые для реального приложения. (Полный исходный код приложения доступен на GitHub .)

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

База данных Тодос

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

 CREATE TABLE `todos` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Primary key for the table.', `title` varchar(256) NOT NULL DEFAULT '' COMMENT 'The text for the todo item.', `complete` bit(1) NOT NULL DEFAULT b'0' COMMENT 'Boolean indicating whether or not the item is complete.', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='To Do items.' 

И вот как может выглядеть таблица после того, как мы поместили в нее некоторые тестовые данные.

Я бы заглавие полный
1 Пример элемента todo в базе данных 0
2 Еще один пример задачи 1
3 Еще один образец todo 0

Как видно из таблицы, наши задачи включают только первичный ключ ( id ), заголовок и бит состояния, чтобы указать, завершены они или нет.

API REST

Наше веб-приложение нуждается в доступе к этой базе данных, поэтому мы предоставим стандартный интерфейс REST. API следует соглашениям Ruby, но может быть легко реализован любой серверной технологией. Особенно:

  • GET api/todos возвращает JSON-кодированный массив всех строк в базе данных.
  • GET api/todos/NNN возвращает представление задачи в формате JSON с id NNN .
  • POST api/todos добавляет новую задачу в базу данных, используя в запросе информацию в кодировке JSON.
  • PUT api/todos/NNN обновляет задачу с id равным NNN используя информацию в кодировке JSON в запросе.
  • DELETE api/todos/NNN удаляет задачу с id равным NNN из базы данных.

Если вы не особенно любите Ruby, исходный код включает полную реализацию этого API на PHP.

Библиотеки JavaScript

Наше скромное приложение достаточно просто для реализации в чистом JavaScript без каких-либо библиотек, но у нас гораздо большие планы. Мы можем начинать с малого, но в конечном итоге приложение будет обладать удивительной функциональностью и восхитительным пользовательским интерфейсом. Готовясь к этому дню, мы будем строить фреймворк, который может поддерживать наше приложение-убийца:

  • jQuery для манипулирования DOM, обработки событий и взаимодействия с сервером.
  • Underscore.js для улучшения базового языка с помощью многих необходимых утилит.
  • Backbone.js для определения структуры приложения с точки зрения моделей и представлений.

Скелет HTML

Теперь, когда мы знаем компоненты, которые будут входить в наше приложение, мы можем определить HTML-каркас, который будет его поддерживать. В этом нет ничего фантастического (пока), только минимальный документ HTML5, несколько файлов JavaScript и небольшой кусочек кода для начала работы.

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title></title> </head> <body> <h1>List of Todos</h1> <script src="lib/jquery-1.9.0.min.js"></script> <script src="lib/underscore-min.js"></script> <script src="lib/backbone-min.js"></script> <script src="src/app-todos.js"></script> <script> $(function () { var todos = new todoApp.Todos(); todos.fetch(); var list = new todoApp.TodosList({collection: todos}); $("body").append(list.el); }) </script> </body> </html> 

Тестирование во время разработки

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

Установка инструментов

Несмотря на то, что мы будем разрабатывать в браузере, наша тестовая среда опирается на node.js. Поэтому самым первым шагом является установка node.js и менеджера пакетов узлов (npm). На веб-сайте node.js есть исполняемые двоичные файлы для OS X, Windows, Linux и SunOS, а также исходный код для других операционных систем. После запуска установщика вы можете проверить и node.js, и npm из командной строки.

 bash-3.2$ node --version v0.8.18 bash-3.2$ npm --version 1.2.2 bash-3.2$ 

Все остальное, что нам нужно, удобно доступно в виде пакета узла. Диспетчер пакетов узла может обрабатывать их установку, а также любые зависимости.

 bash-3.2$ npm install jquery jsdom underscore backbone mocha chai sinon sinon-chai testem -g 

Создание структуры проекта

Исходный код этого примера включает полную структуру проекта со следующими 15 файлами:

 todos.html testem.json api/htaccess api/todos.php lib/backbone-min.js lib/chai.js lib/jquery-1.9.0.min.js lib/sinon-1.5.2.js lib/sinon-chai.js lib/underscore-min.js mysql/todos.sql php-lib/dbconfig.inc.php src/app-todos.js test/app-todos-test.js test/mocha.opts 

Вот что содержит каждая папка и файл:

  • todos.html : скелетный HTML-файл для нашего приложения, показанный полностью выше.
  • testem.json : файл конфигурации для Test’Em; мы рассмотрим это подробно в ближайшее время.
  • api/ : папка для нашей реализации REST API.
    • api/htaccess : пример конфигурации для веб-сервера Apache, который поддерживает наш REST API.
    • api/todos.php : PHP-код для реализации REST API.
  • lib/ : папка для библиотек JavaScript, используемых самим приложением и тестовой средой.
    • lib/backbone-min.js : минимизированная версия Backbone.js.
    • lib/chai.js : библиотека утверждений Чай.
    • lib/jquery-1.9.0.min.js : минимизированная версия jQuery.
    • lib/sinon-1.5.2.js : библиотека Sinon.JS.
    • lib/sinon-chai.js : Sinon.JS Утверждения для Чая.
    • lib/underscore-min.js : минимизированная версия Underscore.js.
  • mysql/ : папка для кода MySQL для приложения.
    • mysql/todos.sql : команды MySQL для создания базы данных приложения.
  • php-lib/ : папка для библиотек PHP и конфигурации для REST API приложения.
    • php-lib/dbconfig.inc.php : конфигурация базы данных PHP для REST API.
  • src/ : папка для нашего клиентского кода приложения.
    • src/app-todos.js : наше приложение.
  • test/ : папка для тестового кода.
    • test/app-todos-test.js : тестовый код для нашего приложения.
    • test/mocha.opts : параметры конфигурации для mocha; мы рассмотрим это в следующем разделе.

Во время разработки нас интересуют только три из этих файлов: testem.json , src/app-todos.js и test/app-todos-test.js .

Настройка Test’Em

Последний шаг перед фактической разработкой — определение конфигурации Test’Em. Эта конфигурация находится в JSON-формате testem.json , и ее достаточно просто создать в любом текстовом редакторе. Мы просто указываем, что используем Mocha (Test’Em поддерживает несколько платформ), и перечисляем файлы JavaScript, которые необходимы нашему приложению и нашему тестовому коду.

 { "framework": "mocha", "src_files": [ "lib/jquery-1.9.0.min.js", "lib/underscore-min.js", "lib/backbone-min.js", "src/*.js", "lib/chai.js", "lib/sinon-chai.js", "lib/sinon-1.5.2.js", "test/*.js" ] } 

Начать разработку

Наконец, мы готовы к кодированию. В командной оболочке перейдите в корневую папку нашего проекта и выполните команду testem . Запустятся сценарии Test’Em, очистив окно терминала и предоставив нам URL в правом верхнем углу. Скопируйте и вставьте этот URL в наш браузер, и мы отключены.

Как только мы запустим веб-браузер, он автоматически выполнит все тесты, которые мы определили. Поскольку мы только начинаем разработку, у нас не будет ни кода, ни тестов. Браузер будет любезно указать нам на это.

Окно терминала, из которого мы запустили Test’Em, также даст нам статус.

Первый тестовый пример

В духе истинной разработки через тестирование мы начнем с написания нашего первого контрольного примера в файле test/app-todos-test.js . Как и любое хорошее веб-приложение, мы хотим минимизировать глобальное загрязнение пространства имен. Для этого мы будем полагаться на одну глобальную переменную todoApp , которая будет содержать весь наш код. Наш первый тестовый пример убедится, что глобальная переменная пространства имен существует.

 var should = chai.should(); describe("Application", function() { it("creates a global variable for the name space", function () { should.exist(todoApp); }) }) 

Как видите, нам нужно одно предварительное утверждение, чтобы сказать Мокко, что мы используем утверждения Чая. Тогда мы можем начать писать тесты. По соглашению тесты JavaScript организованы в блоки (которые могут быть вложены в подблоки и т. Д.). Каждый блок начинается с вызова функции description describe() чтобы определить, какую часть кода мы тестируем. В этом случае мы тестируем приложение в целом, так что это первый параметр describe() .

В тестовом блоке мы документируем каждый тестовый пример по тому, что он тестирует. Это цель функции it() . Способ чтения любого контрольного примера — объединить строки describe() и it() в один оператор. Поэтому наш первый тестовый пример

Приложение создает глобальную переменную для пространства имен

Сам тестовый код находится внутри блока it() . Наш тестовый пример

 should.exist(todoApp); 

Теперь у нас есть полный контрольный пример. Как только мы сохраняем файл, Test`Em автоматически вступает во владение. Он замечает, что один из наших файлов изменился, поэтому он немедленно перезапускает тесты. Не удивительно (так как мы еще не написали код для приложения), наш первый тест не пройден.

Окно терминала также обновляется автоматически.

Чтобы пройти тест, мы должны создать глобальную переменную пространства имен. srcapp-todos.js файлу srcapp-todos.js и добавляем необходимый код.

 if (typeof todoApp === "undefined") todoApp = {}; 

Как только мы сохраняем файл, Test`Em снова вступает в действие. Мы немедленно получаем обновленные результаты для наших тестов.

Сделайте шаг назад и подумайте, что происходит! Каждый раз, когда мы вносим изменения в код теста или в наше приложение, Test`Em немедленно перезапускает весь наш набор тестов. Все, что нам нужно сделать, это держать браузер Test’Em или окно терминала видимым в углу нашего экрана, и мы можем видеть работоспособность нашего кода в реальном времени по мере разработки . Мы узнаем, как только мы представим ошибку, даже если ошибка проявляется в той части кода, которая отличается от того, где мы работаем. Больше не нужно копаться в часах, днях или неделях нового кода, чтобы выяснить, когда мы представили ошибку.

Тестирование модели

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

 describe("Todo Model", function(){ describe("Initialization", function() { beforeEach(function() { this.todo = new todoApp.Todo(); }) it("should default the status to 'pending'",function() { this.todo.get('complete').should.be.false; }) it("should default the title to an empty string",function() { this.todo.get('title').should.equal(""); }) }) }) 

Есть несколько аспектов этих тестов, которые стоит отметить.

  • Мы можем вкладывать тестовые блоки друг в друга. Один тестовый блок будет содержать все модульные тесты для модели todo, а подблок этих тестов сосредоточен на инициализации.
  • Внутри тестового блока мы можем определить функциональность, выполняемую перед каждым тестом. Это цель блока beforeEach() . В приведенном выше примере мы создаем новый экземпляр Todo перед каждым тестом.
  • Платформа Mocha автоматически гарантирует, что контекст JavaScript (то есть значение this ) является единым для всех наших тестовых случаев. Вот почему мы можем определить this.todo в одной функции (параметр beforeEach() ) и безопасно ссылаться на него в других функциях (таких как параметры it() ). Если бы Mocha не работал за кулисами для обеспечения такой согласованности, JavaScript определял бы разные контексты для каждой функции.

Конечно, поскольку мы еще не написали код модели, все наши тесты не пройдут. (И мы это сразу узнаем.) Но как только мы добавим код для нашей модели, тесты пройдут, и мы уже в пути.

 todoApp.Todo = Backbone.Model.extend({ defaults: { title: "", complete: false } }) 

Использование заглушек для сторонних функций

Теперь, когда у нас есть простая модель для задач, мы можем начать определять ее поведение. Наша модель должна обновлять базу данных всякий раз, когда изменяется любое из ее свойств. Однако в среде модульного тестирования у нас не будет реальной базы данных для проверки. С другой стороны, мы на самом деле не пишем код для обновления базы данных. Скорее, мы полагаемся на Backbone для управления этим взаимодействием. Это предполагает стратегию модульного тестирования для этого теста. Все, что нам нужно знать, — это то, что в моделях Backbone используется метод save() для обновления любого сохраняемого хранилища модели. В нашем случае этим резервным хранилищем является база данных. Вот код модульного теста, который мы можем использовать:

 describe("Persistence", function() { beforeEach(function() { this.todo = new todoApp.Todo(); this.save_stub = sinon.stub(this.todo, "save"); }) afterEach(function() { this.save_stub.restore(); }) it("should update server when title is changed", function() { this.todo.set("title", "New Summary"); this.save_stub.should.have.been.calledOnce; }) it("should update server when status is changed", function() { this.todo.set('complete',true); this.save_stub.should.have.been.calledOnce; }) }) 

Мы добавили некоторый дополнительный код перед каждым тестом, и мы добавили раздел кода для выполнения после каждого теста. Этот дополнительный код управляет stub sinon, функцией, которая фактически аннулирует другую функцию в коде. В нашем случае заглушка аннулирует метод save() этого this.todo . С заглушкой на месте вызовы метода на самом деле не будут передаваться в библиотеку Backnone. Вместо этого sinon перехватывает эти вызовы и просто немедленно возвращается. Это поведение важно. Если бы мы попытались выполнить настоящий метод Backbone save() в среде модульного тестирования, вызов не состоялся бы, потому что не было бы доступного API базы данных или сервера.

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

 todoApp.Todo = Backbone.Model.extend({ defaults: { title: "", complete: false }, initialize: function() { this.on("change", function(){ this.save(); }); } }) 

Тестирование вида

Конечно, наше приложение никому не принесет пользы, если оно на самом деле не отображает задачи для пользователей, а для этого нужно создать некоторый HTML. Мы будем использовать представления Backbone для этой функциональности. В нашем простом приложении мы просто хотим отобразить каждую задачу в виде элемента списка. Вот тестовые примеры, которые помогут нам начать.

 describe("Todo List Item View", function() { beforeEach(function(){ this.todo = new todoApp.Todo({title: "Summary"}); this.item = new todoApp.TodoListItem({model: this.todo}); }) it("render() should return the view object", function() { this.item.render().should.equal(this.item); }); it("should render as a list item", function() { this.item.render().el.nodeName.should.equal("LI"); }) }) 

Мы начинаем наши тесты представления с двух тестовых случаев. Сначала мы гарантируем, что метод render() возвращает само представление. Это распространенное и очень удобное соглашение в Backbone, потому что оно позволяет создавать цепочки методов. Наш второй тестовый пример проверяет, что HTML-элемент, который создает рендер, является элементом списка ( <li> ). Код, необходимый для прохождения этих тестов, представляет собой простое представление Backbone.

 todoApp.TodoListItem = Backbone.View.extend({ tagName: "li", render: function() { return this; } }) 

Далее мы можем разработать подробное содержание этого элемента списка. В качестве примера, мы хотим, чтобы элемент полного списка выглядел примерно так:

 <li> <label> <input type='checkbox'/> Summary </label> </li> 

В наших тестах мы можем использовать jQuery для извлечения отдельных элементов из основного элемента представления.

 describe("Todo List Item View", function() { beforeEach(function(){ this.todo = new todoApp.Todo({title: "Summary"}); this.item = new todoApp.TodoListItem({model: this.todo}); }) describe("Template", function() { beforeEach(function(){ this.item.render(); }) it("should contain the todo title as text", function() { this.item.$el.text().should.have.string("Summary"); }) it("should include a label for the status", function() { this.item.$el.find("label").should.have.length(1); }) it("should include an <input> checkbox", function() { this.item.$el.find("label>input[type='checkbox']").should.have.length(1); }) it("should be clear by default (for 'pending' todos)", function() { this.item.$el.find("label>input[type='checkbox']").is(":checked").should.be.false; }) it("should be set for 'complete' todos", function() { this.save_stub = sinon.stub(this.todo, "save"); this.todo.set("complete", true); this.item.render(); this.item.$el.find("label>input[type='checkbox']").is(":checked").should.be.true; this.save_stub.restore(); }) }) }) 

Обратите внимание, что в последнем тестовом примере мы использовали метод save() . Поскольку мы изменяем свойство со значения по умолчанию, наша модель должным образом попытается сохранить это изменение в своем резервном хранилище. Однако в среде модульных тестов у нас не будет базы данных или серверного API. Заглушка заменяет отсутствующие компоненты и позволяет тестам проходить без ошибок. Чтобы пройти эти тесты, нам нужно добавить дополнительный код в наше представление.

 todoApp.TodoListItem = Backbone.View.extend({ tagName: "li", template: _.template( "<label>" + "<input type='checkbox' <% if(complete) print('checked') %>/>" + " <%= title %> " + "</label>"), render: function() { this.$el.html(this.template(this.model.attributes)); return this; } }) 

Модель тестирования / Просмотр взаимодействий

Теперь, когда мы убедились, что наша реализация представления создает правильную разметку HTML, мы можем проверить ее взаимодействие с нашей моделью. В частности, мы хотим убедиться, что пользователи могут переключать статус задачи, установив флажок. Наша тестовая среда не требует реального пользователя, поэтому мы будем использовать jQuery для генерации события click. Однако для этого нам нужно будет добавить контент в реальный DOM. Этот контент известен как тестовое устройство. Вот код модульного теста.

 describe("Todo List Item View", function() { beforeEach(function(){ this.todo = new todoApp.Todo({title: "Summary"}); this.item = new todoApp.TodoListItem({model: this.todo}); this.save_stub = sinon.stub(this.todo, "save"); }) afterEach(function() { this.save_stub.restore(); }) describe("Model Interaction", function() { it("should update model when checkbox clicked", function() { $("<div>").attr("id","fixture").css("display","none").appendTo("body"); this.item.render(); $("#fixture").append(this.item.$el); this.item.$el.find("input").click(); this.todo.get('complete').should.be.true; $("#fixture").remove(); }) }) }) 

Обратите внимание, что мы снова заглушаем метод todo save() . В противном случае Backbone попытается обновить несуществующее резервное хранилище, когда мы изменим статус задачи с помощью нашего имитированного клика.

Для самого теста мы начинаем с создания элемента <div> с id fixture и добавляем этот элемент в наш рабочий документ. Живым документом, в данном случае, является веб-страница, отображающая результаты наших тестов. Хотя мы удаляем элемент сразу после проверки контрольного примера, мы также устанавливаем его свойство display равным none чтобы оно не мешало отображению результатов теста Mocha. Код, который реализует эту функциональность, включает небольшое дополнение к модели todo. Дополнением является новый toggleStatus() .

 todoApp.Todo = Backbone.Model.extend({ defaults: { title: "", complete: false }, initialize: function() { this.on("change", function(){ this.save(); }); }, toggleStatus: function() { this.set("complete",!this.get("complete"")); } }) 

В представлении мы хотим отловить события нажатия на элементе <input> и вызвать этот метод для модели.

 todoApp.TodoListItem = Backbone.View.extend({ tagName: "li", template: _.template( "<label>" + "<input type='checkbox' <% if(complete) print('checked') %>/>" + " <%= title %> " + "</label>"), events: { "click input": "statusChanged" }, render: function() { this.$el.html(this.template(this.model.attributes)); return this; }, statusChanged: function() { this.model.toggleStatus(); } }) 

Тестирование коллекции

На данный момент наша заявка почти завершена. Единственная оставшаяся функциональность — собирать все задачи вместе. Естественно, мы будем использовать коллекцию Backbone. На самом деле мы не собираемся делать что-то особенное с нашей коллекцией, поэтому нам не нужны никакие юнит-тесты.

 todoApp.Todos = Backbone.Collection.extend({ model: todoApp.Todo, url: "api/todos" }) 

Однако мы можем проверить, что наша реализация представления коллекции является подходящей. Мы хотим, чтобы это представление отображалось как неупорядоченный список ( <ul> ). Тестовые случаи не требуют никакой функциональности, которую мы не видели раньше.

 describe("Todos List View", function() { beforeEach(function(){ this.todos = new todoApp.Todos([ {title: "Todo 1"}, {title: "Todo 2"} ]); this.list = new todoApp.TodosList({collection: this.todos}); }) it("render() should return the view object", function() { this.list.render().should.equal(this.list); }); it("should render as an unordered list", function() { this.list.render().el.nodeName.should.equal("UL"); }) it("should include list items for all models in collection", function() { this.list.render(); this.list.$el.find("li").should.have.length(2); }) }) 

Реализация представления также проста. Он отслеживает любые дополнения к коллекции и обновляет вид. Для начального render() он просто добавляет все модели в коллекцию по одной.

 todoApp.TodosList = Backbone.View.extend({ tagName: "ul", initialize: function() { this.collection.on("add", this.addOne, this); }, render: function() { this.addAll(); return this; }, addAll: function() { this.collection.each(this.addOne, this); }, addOne: function(todo) { var item = new todoApp.TodoListItem({model: todo}); this.$el.append(item.render().el); } }) 

Бонусные тесты: проверка API

Поскольку наш REST API полностью соответствует API, который ожидает Backbone, нам не требовался какой-либо специальный код для управления взаимодействием API. В результате нам не нужны какие-либо юнит-тесты. В реальном мире вам может быть не так повезло. Если ваш API не соответствует соглашениям Backbone, вам может потребоваться переопределить или расширить некоторый код Backbone для работы с нестандартным API. Этот дополнительный код также потребует модульных тестов. К счастью, тестировать взаимодействия API довольно легко даже в среде модульного тестирования.

Самый простой способ проверить взаимодействие API-интерфейса — использовать поддельные функции сервера Sinon.JS. К сожалению, эта функциональность доступна (в настоящее время) только в реализации браузера Sinon. Это явно исключено из реализации node.js. Есть несколько хаков, чтобы запустить его в node.js, но эти хаки довольно хрупкие и полагаются на внутренние детали реализации. Было бы лучше избегать их, если это возможно. К счастью, мы можем обойтись без поддельного сервера Синона.

Секрет в том, что Backbone использует функцию jQuery $.ajax() для реализации API REST. Мы можем перехватить взаимодействия API, заглушив эту функцию. Когда мы заглушаем функцию, мы хотим заменить наш собственный ответ. Метод yieldsTo() дает нам именно такую ​​возможность. Он сообщает sinon, какие дополнительные действия необходимо предпринять, когда вызывается заглушка. Вот полный тестовый пример, чтобы убедиться, что наша коллекция правильно инициализирует себя с помощью REST API.

 describe("Collection's Interaction with REST API", function() { it("should load using the API", function() { this.ajax_stub = sinon.stub($, "ajax").yieldsTo("success", [ { id: 1, title: "Mock Summary 1", complete: false }, { id: 2, title: "Mock Summary 2", complete: true } ]); this.todos = new todoApp.Todos(); this.todos.fetch(); this.todos.should.have.length(2); this.todos.at(0).get('title').should.equal("Mock Summary 1"); this.todos.at(1).get('title').should.equal("Mock Summary 2"); this.ajax_stub.restore(); }) }) 

Законченный!

Как видно из следующего снимка экрана, теперь мы написали код, который проходит все тестовые примеры. По крайней мере, на данный момент разработка завершена.

Тестирование во время интеграции

Теперь, когда разработка нашего приложения на стороне клиента завершена (и у нас есть тесты, чтобы это доказать), мы можем смело использовать наш JavaScript в системе управления исходным кодом. Затем его можно интегрировать в процесс сборки для всего приложения. В рамках этого процесса мы хотим выполнить все разработанные тесты. Это гарантирует, что код, составляющий окончательное развертывание, пройдет все тесты, которые мы определили. Это также защитит от «мелких изменений» в коде, которые непреднамеренно вносят новые ошибки.

В процессе сборки мы, скорее всего, захотим выполнять наши тесты из командной строки, а не из веб-браузера. Нам не нужны детали отдельных тестовых случаев, просто гарантия того, что они все пройдут. Node.js позволяет достаточно легко удовлетворить это требование. Нам нужно всего лишь сделать несколько небольших дополнений к нашему исходному коду и файлам кода модульного тестирования.

Наш код нуждается в этих модификациях, потому что node.js обрабатывает глобальные переменные иначе, чем веб-браузеры. В веб-браузере переменные JavaScript по умолчанию являются глобальными. Node.js, с другой стороны, ограничивает переменные локальным модулем по умолчанию. В этой среде наш код не сможет найти необходимые сторонние библиотеки (jQuery, Underscore и Backbone. Однако, если мы добавим следующие операторы в начале, node.js разрешит ссылки на эти библиотеки соответствующим образом Мы создали эти операторы так, чтобы они не причиняли вреда веб-браузеру, поэтому мы можем оставить их в коде навсегда.

 var jQuery = jQuery || require("jquery"); var _ = _ || require("underscore"); var Backbone = Backbone || require("backbone"); Backbone.$ = jQuery; 

Нам также нужно настроить наш тестовый код. Сценарии тестирования должны иметь доступ к своим собственным библиотекам (jQuery, Chai, Sinon.JS и sinon-chai). Кроме того, нам нужно добавить немного больше, чтобы имитировать объектную модель документа (DOM) веб-браузера. Напомним, что наши тесты для обработки кликов потребовали от нас временно добавить «крепление» <div> на веб-страницу. Node.js, конечно, обычно не имеет веб-страницы. Однако пакет узла jsdom позволяет нам эмулировать один. Код ниже создает минимальную, смоделированную веб-страницу для наших тестов.

 if (typeof exports !== 'undefined' && this.exports !== exports) { global.jQuery = require("jquery"); global.$ = jQuery; global.chai = require("chai"); global.sinon = require("sinon"); chai.use(require("sinon-chai")); global.jsdom = require("jsdom").jsdom; var doc = jsdom("<html><body></body></html>"); global.window = doc.createWindow(); } 

Условие, которое переносит эти операторы, проверяет, работаем ли мы в среде node.js вместо веб-браузера. В браузере дополнительные операторы не нужны, поэтому мы можем спокойно их пропустить.

С этими изменениями мы можем выполнить полный набор тестов из командной строки. Просто перейдите в корневую папку проекта и выполните команду mocha . Результат выглядит довольно знакомым.

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

Вывод

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

Ресурсы

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