Статьи

Получение вашего Javascript под контролем

js_control Вы помните официальное объявление о конвейере активов на RailsConf 2011? Я думаю, это было довольно круто. Как и почти все в Rails, некоторые любили это, а другие ненавидели. В любом случае, он сделал волну, а затем DHH объявил CoffeeScript и SCSS по умолчанию. BOOOM!

Давайте будем честными, в 2013 году, спустя почти 2 года с момента выхода Rails 3.1, мы больше не думаем об этом. Это просто стало частью нашего рабочего процесса при разработке приложений на Rails. Без лишних усилий мы получаем повышение производительности на клиентской стороне наших приложений. Мы пишем гораздо лучше, практикуем JavaScript на основе CoffeeScript и больше не настраиваем упаковщики ресурсов для объединения и минимизации JavaScript и CSS. Это просто случается.

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

Экспоненциальные улучшения в механизмах JavaScript, встроенных в современные браузеры, позволили сложным JS-структурам развиваться и процветать в виде красивых, динамичных и отзывчивых веб-приложений.

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

С великой державой, Яда, Яда, Яда

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

Руки вверх, если тест-драйв вашего JavaScript?

В эти дни я все еще ожидаю увидеть только несколько рук. Я, конечно, не тестировал диск около года назад. У меня были тесты, но обычно они писались намного позже кода. Они были хрупкими и неуправляемыми, в лучшем случае, когда я их писал. Затем наступил день, когда меня задело неприятная ошибка среди беспорядка jQuery и различных (и не говоря уже о многочисленных) связанных плагинов. Я был далеко за гранью неуправляемости. Самое гуманное, что нужно было сделать — это положить бедняжку на пол.

Я решил использовать какой-то фреймворк ( Backbone ) и тестовый фреймворк ( Jasmine ). Даже для тривиальных задач я бы создал представление или модель Backbone, провел его через тесты и применил. Красота чего-то вроде Backbone заключалась в том, что благодаря его использованию по собственному желанию я мог применить его ко всем случаям использования независимо от того, насколько сложен (или нет) сценарий. Я мог бы также избавиться от одного логического куска за раз вместо полной войны пламени.

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

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

Поскольку мы будем использовать Jasmine в качестве тестовой среды, мы будем использовать гем jasminerice . Это избавляет нас от боли, которую мы чувствуем при работе с активами. Чтобы запустить jasminerice, просто Gemfile камень в свой Gemfile

 group :development, :test do gem "jasminerice" end 

И запустите генератор rails g jasminerice:install который установит каталог javascripts в вашем каталоге spec (при условии, что вы используете rspec) и несколько вспомогательных файлов, фиктивный тест и некоторые приспособления. Приспособления очень важны при тестировании наших базовых представлений. Вы увидите, как мы используем их через минуту.

После настройки jasminerice в своем приложении удалите фиктивные тесты, которые были установлены, и создайте приспособление с именем spec/javascripts/fixtures/flash.html со следующим содержимым

 <div class="flash"><p>There is a message for the user <span class="close-alert">X</span></p></div> 

Этот прибор по сути является разметкой, которую мы будем использовать в нашем приложении. Он может немного отличаться или сильно отличаться, но основными ингредиентами являются flash класса на внешнем div и close-alert о close-alert на span внутри. Теперь у нас достаточно, чтобы написать тест.

Создайте файл spec/javascripts/flash_spec.js.coffee со следующим содержимым.

 describe "Closing the alert box", -> beforeEach -> loadFixtures 'flash' it 'will close on a click of the cross', -> view = new App.Flash() $('.close-alert').trigger('click') expect(view.$el).not.toBeVisible() 

В приведенной выше спецификации мы сначала загружаем прибор в DOM, а это означает, что мы помещаем его на страницу запуска спецификации для не инициированных. Jasminerice включает вспомогательные loadFixtures которые создают контейнерные jasmine-fixtures и добавляют в него загруженные приборы. Когда спецификация завершена, она удаляет прибор из DOM, довольно изящного помощника.

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

Для тех, кто не имеет большого опыта работы с Backbone, $el является контейнером для представления. По умолчанию это обычный старый div , но вы можете установить его как хотите. Я не буду останавливаться на этом слишком подробно, но просто знайте, что свойство $el вы видите в вышеприведенных тестах, является помощником Backbone для передачи селектора для элемента контейнера, он эквивалентен $('<your-selector') в jQuery. Но поскольку мы никогда не узнаем <your-selector> представления (динамически установленного или нескольких элементов в одном представлении в коллекции), предпочтительно использовать $el .

Запустите пакет, указав в браузере адрес http: // localhost: 3000 / jasmine . Как и ожидалось, мы получили красный пакет с достаточно действительным отказом. Если мы немного app/assets/javascripts/app.js.coffee наш первый проход, чтобы получить зеленый цвет, создайте файл app/assets/javascripts/app.js.coffee который будет выглядеть примерно так:

 window.App = {} App.Flash = Backbone.View.extend el: '.flash' events: { 'click .close-alert': 'closeAlert' } closeAlert: -> this.$el.fadeOut() 

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

Затем мы расширяем представление Backbone, устанавливая свойство el и привязывая событие click к его обработчику. Вы должны любить простоту Backbone (особенно если смотреть в форме CoffeeScript). Мне особенно нравится, как события связаны с областями представлений, поэтому больше не нужно добавлять уникальные идентификаторы ко всем в обработчиках e.currentTarget .

Запустив тест снова, облом, мы все еще краснеем. В чем дело? JQuery не работает? Конечно нет, помните, что мы имеем дело с тестом на событие. Время анимации jQuery по умолчанию на fadeOut составляет 400 мс. Наш тест запускает событие и почти сразу запускает ожидание. Давайте исправим это, используя подход невежественного эквивалента сна:

 it 'will close on a click of the cross', -> view = new App.Flash() $('.close-alert').trigger('click') waits 410 runs -> expect(view.$el).not.toBeVisible() it 'will close on a click of the cross', -> 

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

Великий Скотт

Настало время представить лучшее, что может случиться с тестированием JavaScript sinon.js . Это шоколад с арахисовым маслом. Sinon — это настоящий сервисный пояс для тестирования JS. Вы можете высмеивать, шпионить, заглушки и вызывать утверждения соответствия. Лучше всего то, что он очень хорошо работает с Jasmine (и всеми остальными тестовыми фреймворками, насколько я знаю). Мы собираемся использовать sinon, чтобы согнуть время и убрать эту неприятную комбинацию ожидания / запуска, замедляющую наш набор тестов.

Чтобы включить Sinon в приложение, просто загрузите sinon.js в только что созданный каталог spec/javascripts/support . И включите следующее в спецификации:

 it 'will close on a click of the cross', -> clock = sinon.useFakeTimers(); view = new App.Flash() $('.close-alert').trigger('click') clock.tick(410) expect(view.$el).not.toBeVisible() 

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

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

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

 it 'will fade slightly when the mouse hovers over it', -> clock = sinon.useFakeTimers(); view = new App.Flash() view.$el.trigger('mouseenter') clock.tick(110) expect(view.$el.css('opacity')).toBe(0.1) 

Использование вышеуказанной спецификации с реализацией следующего просто не сработает.

 App.Flash = Backbone.View.extend el: '.flash' events: { 'click .close-alert': 'closeAlert', 'mouseenter': 'mouseOver' } closeAlert: -> this.$el.fadeOut() mouseOver: -> this.$el.fadeTo(100, 0.1) 

По сути, точность значения непрозрачности будет немного меньше. Таким образом, мы будем использовать сопоставитель toBeCloseTo для определения точности тестируемого значения, в этом случае 0,01 достаточно для меня.

 expect(view.$el.css('opacity')).toBeCloseTo(0.01, 0.1) 

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

 expect(parseInt(view.$el.css('opacity'))).toBe(1) 

Красный, Зеленый, Рефакторинг, Рефакторинг Тесты

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

 describe "flash alert box", -> beforeEach -> loadFixtures 'flash' @view = new App.Flash() @clock = sinon.useFakeTimers() @fireEvent = (event, element, clockTick=500)-> element.trigger(event) @clock.tick(clockTick) it 'will close on a click of the cross', -> @fireEvent('click', $('.close-alert')) expect(@view.$el).not.toBeVisible() it 'will fade slightly when the mouse hovers over the container', -> @fireEvent('mouseenter', @view.$el) expect(@view.$el.css('opacity')).toBeCloseTo(0.01, 0.1) it 'will fade back in when the mouse leaves the container', -> @fireEvent('mouseleave', @view.$el) expect(parseInt(@view.$el.css('opacity'))).toBe(1) 

Как вы можете видеть из рефакторированного теста (точно так же, как и рефакторинг rspec- тестов), мы переносим все дублированные настройки в функцию beforeEach . Мы также создали вспомогательный @fireEvent который принимает событие, элемент DOM и тик часов. Тик часов, который я сделал опциональным, по умолчанию равен 500, так как мы используем sinon для фальсификации часов, нам не нужно слишком беспокоиться об этом, но, безусловно, стоит помнить о значении по умолчанию.

Завершение

Теперь у нас есть пара приемов, чтобы протестировать наш клиентский код. Я был бы очень шокирован, если бы вы не подумали: «Это много кода для написания простого флеш-баннера» и, в некоторой степени, вы правы. Как разработчики, мы все должны делать вещи. Но почему мы подвергаем сомнению тесты для простого JavaScript, а не для таких вещей, как rspec shoulda-matchers, it { should belong_to other_object } . Если вы сомневаетесь, круто. В конце дня мы должны оценить и оценить, какое значение дает нам тест.

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

В какой-то момент я, возможно, захочу ajax-ify флэш при отправке формы, имея его уже в хорошей структуре Backbone, что облегчит этот переход. Также в демонстрационном коде есть ошибка. Это маленький, но я знаю, что, имея тесты на месте, будет легко отменить (mouseenter-close-wait-mouseleave).

Следующая часть серии будет посвящена методам и советам по тестированию, а также получению нашего JavaScript в рамках Continuous Integration (CI). Затем мы завершим серию услуг для (DUN DUN DUN) Internet Explorer и под этим я подразумеваю тестирование нашего кросс-браузера / платформы JavaScript.