Большая организация веб-приложений backbone.js

В настоящее время я работаю над большим веб-приложением, построенным на backbone.js, и у меня было много проблем с организацией, «зомби» и т. д., поэтому я решил провести серьезный рефакторинг кода. Я уже написал кучу вспомогательных функций для борьбы с «зомби»; однако я бы хотел начать с самого начала и создать хорошую структуру/организацию кода. Я не нашел много отличных руководств/примеров по крупномасштабной организации backbone.js, поэтому я начал с нуля и хотел бы узнать, смогу ли я получить некоторые мнения о том, с чего я начал.

Я, очевидно, установил свой код в глобальном пространстве имен; но я также хотел бы, чтобы это пространство имен было довольно чистым. В моем основном app.js файлы классов хранятся отдельно от глобального пространства имен; вы можете зарегистрировать класс (чтобы его можно было создать) с помощью функции reg (), а функция inst () создает экземпляр класса из массива классов. Таким образом, помимо трех методов, в пространстве имен MyApp есть только маршрутизатор, модель и представление:

  var MyApp = (function () {var classes = {Routers: {}, Collections  : {}, Модели: {}, Представления: {}}; методы = {init: function () {MyApp.Router = MyApp.inst ('Маршрутизаторы', 'Приложение'); MyApp.Model = MyApp.inst ('  Models ',' App '); MyApp.View = MyApp.inst (' Views ',' App '); Backbone.history.start ();}, reg: function (тип, имя, C) {классы [тип]  [name] = C;}, inst: function (type, C, attrs) {return new classes [type] [C] (attrs || {});}}; return methods;} ()); $ (MyApp  .init);  

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

  MyApp.reg ('Models', 'App', Model);  

Не кажется ли это ненужным способом организации кода? Есть ли у других лучшие примеры организации действительно больших проектов с множеством маршрутизаторов, коллекций, моделей и представлений?


Недавно я работал над проектом Backbone под названием GapVis (здесь код, здесь визуализированный контент). Я не знаю, «действительно ли он большой», но он большой и относительно сложный — 24 класса представления, 5 маршрутизаторов и т. Д. Возможно, стоит взглянуть, хотя я не знаю, что все мои подходы будут соответствующий. Вы можете увидеть некоторые из моих мыслей в длинном вступительном комментарии в моем основном файле app.js. Несколько ключевых архитектурных вариантов:

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

  • Мои маршрутизаторы обрабатываются как специализированные представления — они реагируют на События пользовательского интерфейса (т.е. ввод URL-адреса) путем обновления состояния, и они реагируют на изменения состояния путем обновления пользовательского интерфейса (например, изменения URL-адреса).

  • I сделайте несколько вещей, похожих на то, что вы предлагаете. В моем пространстве имен есть функция init , аналогичная вашей, и объект settings для констант. Но я также поместил большую часть классов моделей и представлений в пространство имен, потому что мне нужно было ссылаться на них в нескольких файлах.

  • Я использую систему регистрации для своих маршрутизаторов и считаю ее для своих представлений хорошим способом сохранить «мастер-классы» ( AppRouter и AppView ) от необходимости знать каждое представление. Однако в случае AppView оказалось, что порядок дочерних представлений важен, поэтому я закончил жесткое кодирование этих классов.

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

2


Эти 2 ресурса помогли мне установить мои базовые приложения на прочном основании:

  • https://github.com/tbranyen/backbone-boilerplate
  • http://ricostacruz.com/backbone-patterns/#namespace_convention

0


Пространство имен, аналогичное тому, что вы делаете (по крайней мере, для часть классов), и все мои модели, представления и контроллеры выглядят следующим образом:

views/blocks.js:

  (function (cn  ) {cn.classes.views.blocks = cn.classes.views.base.extend ({events: {}, blocksTemplate: cn.helpers.loadTemplate ('tmpl_page_blocks'), инициализировать: f  unction () {}, визуализация: функция () {$ (this.el) .html (this.blocksTemplate ());  }, registerEvents: function () {}, unregisterEvents: function () {}});}) (название компании);  

Мое пространство имен JavaScript выглядит так, хотя я улучшаю после этого каждый раз, когда я создаю новое приложение:

  название компании: {$: function () {},  

Все, что я хочу, чтобы все мои представления имели, я помещаю в базовое представление. Мой контроллер будет вызывать registerEvents в любом новом представлении, которое он создает (после рендеринга), и unregisterEvents в представлении прямо перед его уничтожением. Не все представления имеют эти два дополнительных метода, поэтому сначала проверяется их существование.

Не забывайте, что все представления имеют встроенный this.el.remove (); . Который не только убивает элемент контейнера представлений, но и отвязывает все связанные с ней события. В зависимости от того, как вы создаете свои представления через контроллер, вы, возможно, не захотите убивать элемент и вместо этого выполните this.el.unbind (), чтобы отвязать все события.

2


На самом деле у разных способов есть свои преимущества и недостатки. Самое главное найти подходящий способ организации файлов. Ниже приводится организация проекта, которым я сейчас занимаюсь. Таким образом, в центре внимания будут те же файлы, относящиеся к модулю, помещенные в папку. Например: модуль people, все файлы этого модуля помещаются в каталог modules/base/people. После обновления и обслуживания этого модуля, нужно сосредоточиться только на файлах в этом каталоге в строке, это не повлияет на файлы вне каталога и улучшит ремонтопригодность.

Я надеюсь мой ответ может вам помочь, надеюсь, вы получите ценный совет.

2



Содержание
  1. Модульное тестирование приложений Backbone.js
  2. Бесплатная книга по JavaScript!
  3. Сборка среды тестирования JavaScript
  4. Среды выполнения
  5. Test Framework
  6. Assertion Library
  7. Шпионы, заглушки и моки
  8. Среда разработки модульных тестов
  9. Пример приложения
  10. База данных Todos
  11. REST API
  12. Библиотеки JavaScript
  13. Скелет HTML
  14. Тестирование во время разработки
  15. Установка инструментов
  16. Создание структуры проекта
  17. Настройка Test’Em
  18. Начать разработку
  19. Первый тестовый пример
  20. Тестирование модели
  21. Использование заглушек для сторонних функций
  22. Тестирование представления
  23. Тестирование взаимодействий между моделью и представлением
  24. Тестирование коллекции
  25. Бонусные тесты: проверка API
  26. Готово!
  27. Тестирование во время интеграции
  28. Заключение
  29. Ресурсы
  30. Стивен Томас
  31. Новые книги уже вышли!
  32. Последние удаленные задания
  33. Представитель по развитию продаж
  34. Интерфейсный веб-разработчик и дизайнер
  35. Старший инженер DevOps по безопасности
  36. Технический менеджер, Trello
  37. Старший интерфейсный разработчик

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

Стивен Томас

JavaScript

Поделиться:

Бесплатная книга по JavaScript!

Пишите мощно, чисто и поддерживаемый JavaScript.

RRP $ 11.95

Получите книгу бесплатно!

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

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

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

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

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

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

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

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

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

Test Framework

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

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

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

Assertion Library

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

  var sum = 2 + 2;  

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

  assert.equal (sum, 4, «сумма должна равняться 4»);  

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

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

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

  sum.should.equal (4);  

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

Шпионы, заглушки и моки

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

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

Наряду с самой библиотекой Sinon.JS мы можем расширить стандарт Библиотека утверждений Chai с утверждениями Sinon.JS для Chai.

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

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

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

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

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

База данных Todos

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

  СОЗДАЙТЕ ТАБЛИЦУ `todos` (` id` int (11) NOT NULL AUTO_INCREMENT COMMENT 'Первичный ключ для таблицы.', `title` varchar (256) NOT NULL DEFAULT  '' COMMENT 'Текст для элемента задачи.', `Complete` бит (1) NOT NULL ПО УМОЛЧАНИЮ b'0 'COMMENT' Логическое значение, указывающее, завершен ли элемент. ', PRIMARY KEY (` id`)) ENGINE  = InnoDB DEFAULT CHARSET = utf8 COMMENT = 'To Do items.'  

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

id title complete
1 Образец задачи в базе данных 0
2 Другой пример задачи 1
3 Еще один пример задачи 0

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

REST API

Наши b приложению требуется доступ к этой базе данных, поэтому мы предоставим стандартный интерфейс 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, исходный код включает полную PHP-реализацию этого API.

Библиотеки JavaScript

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

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

Скелет HTML

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

          

Список задач

$ (function () {var todos = new todoApp.Todos (); todos.fetch (); var list = new todoApp .TodosList ({collection: todos}); $ ("body"). Append (list.el);})

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

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

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

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

  bash-3.2 $ node --versionv0.8.18bash-3.2 $ npm -  version1.2.2bash-3.2 $  

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

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

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

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

   todos.htmltestem.jsonapi/htaccessapi/todos.phplib/backbone-min.jslib/chai.jslib/jquery-1.9.0.min.jslib/sinon-1.5.2.jslib/sinon-chai.jslib /underscore-min.jsmysql/todos.sqlphp-lib/dbconfig.inc.phpsrc/app-todos.jstest/app-todos-test.jstest/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 : библиотека утверждений Chai.
    • lib/jquery-1.9. 0.min.js : уменьшенная версия jQuery.
    • lib/sinon-1.5.2.js : библиотека Sinon.JS.
    • lib/sinon-chai.js : утверждения Sinon.JS для Chai.
    • lib/underscore-min .js : уменьшенная версия Underscore.js.
  • mysql/: папка для кода MySQL для application.
    • mysql/todos.sql : команды MySQL для создания базы данных приложения.
  • php-lib/: папка для библиотек PHP и конфигурации для REST API приложения.
    • php-lib/dbconfig. inc.php : конфигурация базы данных PHP для REST API.
  • src/: папка для нашего клиента код приложения.
    • src/app-todos.js : наше приложение.
  • test/: папка для тестового кода.
    • t est/app-todos-test.js : тестовый код для нашего приложения.
    • test/mocha.opts : параметры конфигурации для мокко; мы рассмотрим это в следующем разделе.

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

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

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

  {"framework"  : «мокко», «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 (  ); описать ("Приложение", function () {it ("создает глобальную переменную для пространства имен", function () {should.exist (todoApp);})})  

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

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

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

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

  should.exist (todoApp);  

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

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

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

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

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

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

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

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

  описать ("Модель Todo", function () {описать ("Инициализация", function ()  {beforeEach (function () {this.todo = new todoApp.Todo ();}) it ("по умолчанию должен иметь статус" ожидающий "", function () {this.todo.get ('complete'). should.  be.false;}) it ("по умолчанию заголовок должен быть пустой строкой", function () {this.todo.get ('title'). should.equal ("");})})})  

Стоит отметить несколько аспектов этих тестов.

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

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

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

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

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

  описать ("Постоянство", function () {beforeEach (function () {this.todo = new todoApp.Todo ()  ; this.save_stub = sinon.stub (this.todo, "save");}) afterEach (function () {this.save_stub.restore ();}) it ("должен обновлять сервер при изменении заголовка", function (  ) {this.todo.set ("title", "New Summary"); this.save_stub.should.have.been.calledOnce;}) it ("должен обновлять сервер при изменении статуса", function () {this.  todo.set ('complete', true); this.save_stub.should.have.been.calledOnce;})})  

Мы включили дополнительный код перед каждым тестом , и мы добавили раздел кода, который будет выполняться после каждого теста. Этот дополнительный код управляет заглушкой 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 для этой функциональности. В нашем тривиальном приложении мы просто хотим отображать каждое задание как элемент списка. Вот тестовые примеры, с которых мы начнем.

  описать ("Просмотр элемента списка Todo", function () {beforeEach (function () {this.todo = new todoApp  .Todo ({title: "Summary"}); this.item = new todoApp.TodoListItem ({model: this.todo});}) it ("render () должен возвращать объект представления", function () {this  .item.render (). should.equal (this.item);}); it ("должен отображаться как элемент списка", function () {this.item.render (). el.nodeName.should.equal (  "LI");})})  

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

  • ). Код, необходимый для прохождения этих тестов, представляет собой простое представление Backbone.

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

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

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

      describe ("Просмотр элемента списка Todo", function () {beforeEach (function () {this.todo = new todoApp.Todo ({title: "Summary"}); this.item = new todoApp.TodoListItem ({  model: this.todo});}) describe ("Шаблон", function () {beforeEach (function () {this.item.render ();}) it ("должен содержать заголовок задачи в виде текста", function (  ) {this.item. $ el.text (). should.have.string ("Сводка");}) it ("должен включать метку для статуса", function () {this.item. $ el.find  ("label"). should.have.length (1);}) it ("должен включать флажок ", function () {this.item. $ el.find ("label> input [type = '  checkbox '] "). should.have.length (1);}) it (" по умолчанию должен быть снят (для "ожидающих" задач) ", function () {this.item. $ el  .find ("метка> input [type = 'checkbox']"). is (": проверено"). should.be.false;  }) it ("должен быть установлен для" завершенных "задач", 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 (" "), render: function () {this. $ el.html (this.template (this. model.attributes));  вернуть это;  }})  

    Тестирование взаимодействий между моделью и представлением

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

      описать (  "Просмотр элемента списка Todo", 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 ();}) описать ("Взаимодействие с моделью", function () {it  ("должна обновлять модель при нажатии флажка", function () {$ ("
    ") .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 ();})})})

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

    Для самого тестового примера мы начинаем с создания с id из fixture , и мы добавляем этот элемент в наш рабочий документ. Живым документом в данном случае является веб-страница, отображающая результаты наших тестов. Хотя мы удаляем элемент сразу после проверки тестового примера, мы также устанавливаем для его свойства display значение none , чтобы он не мешал отображению теста Mocha. полученные результаты. Код, реализующий эту функциональность, включает небольшое дополнение к модели задач. Добавлен новый метод 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" "));}})  

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

      todoApp.TodoListItem = Backbone.View.extend ({tagName: "li", template: _. шаблон ("")  , events: {"click input": "statusChanged"}, render: function () {this. $ el.html (this.template (this.model.attributes));  вернуть это;  }, statusChanged: function () {this.model.toggleStatus ();  }})  

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

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

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

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

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

      описать ("Просмотр списка задач", function () {beforeEach (function () {this  .todos = new todoApp.Todos ([{title: "Todo 1"}, {title: "Todo 2"}]); this.list = new todoApp.TodosList ({collection: this.todos});})  ("render () должен возвращать объект представления", function () {this.list.render (). should.equal (this.list);}); it ("должен отображаться как неупорядоченный список", function ()  {this.list.render (). el.nodeName.should.equal ("UL");}) it ("должен включать элементы списка для всех моделей в коллекции", 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 ("добавить", this.addOne, this);}, render: function () {this.addAll (); вернуть это;  }, 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, но эти хаки довольно хрупкие и зависят от внутренних деталей реализации. По возможности лучше их избегать. К счастью, мы можем обойтись и без поддельного сервера Sinon.

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

      описать ("Взаимодействие коллекции с REST API", function () {it ("должно  загрузить с помощью 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 ("Краткое описание 1"); this.todos.at (1) .get ('title'). should.equal ("  Макет резюме 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 ("подчеркивание"); var Backbone = Backbone || require ("backbone"); Backbone. $ = jQuery;  

    Нам также необходимо скорректировать наш тестовый код. Тестовым скриптам нужен доступ к их собственным библиотекам (jQuery, Chai, Sinon.JS и sinon-chai). Кроме того, нам нужно добавить немного больше для имитации объекта документа веб-браузера. Модель (DOM). Напомним, что наши тесты для обработки кликов требовали, чтобы мы временно добавили на веб-страницу «приспособление»

    . Node.js, конечно, обычно не имеет веб-страницу. Однако пакет jsdom node позволяет подражать одному. Приведенный ниже код создает минимальную смоделированную веб-страницу для наших тестов.
      if (typeof exports! == 'undefined' && this.exports! == exports) {global.  jQuery = require ("jquery");  глобальный. $ = jQuery;  global.chai = require ("chai");  global.sinon = require ("sinon");  chai.use (требуется ("синон-чай"));  global.jsdom = require ("jsdom"). jsdom;  var doc = jsdom ("   ");  global.window = doc.createWindow ();}   

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

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

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

    Заключение

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

    Ресурсы

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

    • Среда выполнения JavaScript в командной строке : node.js
    • Среда для модульного тестирования JavaScript: Mocha
    • Среда разработки тестов: Test'em
    • Библиотека утверждений JavaScript: Chai Assertion Библиотека
    • Шпионы, заглушки и насмешки: Sinon.JS
    • Дополнительные утверждения: утверждения Sinon.JS для Chai

    Стивен Томас

    Стивен Томас - фронтенд-архитектор компании из списка Fortune 50, где он отвечает за все аспекты клиентских веб-технологий.

    Новые книги уже вышли!

    Получите практический совет для Начни карьеру в программировании!


    iv>

    Освойте сложные переходы, преобразования и анимацию в CSS!

    Последние удаленные задания

    Представитель по развитию продаж

    Flippa

    • crm
    • таблицы Google

    Интерфейсный веб-разработчик и дизайнер

    Encamp

    • html
    • css

    Старший инженер DevOps по безопасности

    OneLogin

    • aws
    • linux

    Технический менеджер, Trello

    Atlassian

    • trello

    Старший интерфейсный разработчик

    Livelike

    • javascript
    • html
    Другие удаленные задания

    Оцените статью
    clickpad.ru
    Добавить комментарий