Статьи

Клетки: более глубокий взгляд на внедрение и тестирование зависимостей

В моем предыдущем посте вы с Скоттом изучили основы Cells, уровня модели представления для Ruby и платформы Rails.

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

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

Отлично, но что нам теперь делать со всем этим?

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

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

Это интриги Скотта.

Список пользователей

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

В Rails эта функция обычно находится в UsersController и его действии #index . Вместо того, чтобы использовать представление контроллера, давайте использовать ячейки для этого.

Ячейка страницы без макета

Для лучшего понимания нам следует начать с UsersController и посмотреть, как оттуда отображается ячейка страницы.

Обратите внимание, что мы все еще используем макет контроллера, чтобы обернуть список пользователей. Это потому, что мы хотим научиться использовать Cells пошагово. Скотту это нравится. Но Скотту нужно помнить, что Cells также может отображать макет приложения, что делает ActionView полностью избыточным! Это мы рассмотрим позже.

 class UsersController < ApplicationController def index render html: cell("user_index_cell"), layout: true end ... 

Все, что делает контроллер, это рендеринг UserIndexCell который мы должны реализовать сейчас. Вы заметили, что в cell вызов не передается модель? Это связано с тем, что при желании ячейки могут самостоятельно собирать данные. Вскоре мы узнаем, как лучше всего обращаться с зависимостями.

Использование render с опцией :html просто вернет переданную строку в браузер. С layout: true что удивительно, оборачивает эту строку в макет контроллера.

Это все специфичные для Rails. Теперь давайте перейдем к фактической ячейке. Новый UserIndexCell будет UserIndexCell в app / cell / user_index_cell.rb в обычной установке:

В Trailblazer ячейки имеют различную структуру имен и структуру каталогов. Пользовательская ячейка Скотта будет называться User::Cell::Index и находиться в app/concepts/user/cell/index.rb , но это совсем другая история для следующего поста.

 class UserIndexCell < Cell::ViewModel def show @model = User.all render end end 

С вводной записью в затылке, это не выглядит слишком новым. Метод show ячейки назначит @model экземпляра @model , вызвав User.all а затем отобразит ее представление.

Итерации в представлениях

В представлении мы можем использовать коллекцию пользователей и отображать красиво отформатированный список пользователей. В обычных ячейках представление находится в app / cell / user_index / show.haml и выглядит следующим образом:

 %h1 All Users %ul - model.each do |user| %li = link_to user.email, user = image_tag user.avatar 

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

Скотт, будучи самоотверженным и самозваным архитектором программного обеспечения, сужает глаза к щелям. Воображаемый галоп проходит за его 23-дюймовым внешним монитором. Silence.

Сейчас ему не нравятся две вещи:

Почему клетка выбирает свою модель? Разве это не может быть зависимость, передаваемая из внешнего мира, такого как контроллер?

И почему взгляд клетки такой грязный? Разве мы не говорили, что взгляды на ячейки не должны быть логичными? Это выглядит как частичное от обычного проекта Rails.

Ты прав, Скотт, и твоя интуиция архитектора заставила тебя задать правильные вопросы.

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

Внешние зависимости

Всякий раз, когда вы назначаете @model вы должны спросить себя: «Разве не лучше, чтобы кто-то другой взял мои данные?» . Вот как это делается в контроллере:

 class UsersController < ApplicationController def index users = User.all render html: cell("user_index_cell", users), layout: true end ... 

Теперь ответственность за поиск подходящего входа для ячейки лежит на контроллере. Хотя Rails MVC далек от реального MVC, именно это и должен делать контроллер.

Теперь мы можем упростить и ячейку:

 class UserIndexCell < Cell::ViewModel def show render end end 

Помните, что первый аргумент, передаваемый в cell , всегда доступен как model внутри ячейки и ее вида. Пожалуйста, не путайте с термином «модель» . Rails неправильно понял, что «модель» — это всегда одна конкретная сущность. В ООП модель — это просто объект, а в нашем контексте это массив постоянных объектов.

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

 %h1 All Users %ul - model.each do |user| %li = link user = avatar user 

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

Помощники == Методы экземпляра

Чтобы link и avatar работали, нам нужно добавить методы экземпляра в ячейку:

 class UserIndexCell < Cell::ViewModel def show render end private def link(user) link_to(user.email, user) end def avatar(user) image_tag(user.avatar) end end 

Вся логика представления теперь хорошо инкапсулирована как методы экземпляра в ячейке. Представление приведено в порядок, и только делегаты «помощникам».

Ну, вроде как, потому что Скотту не нравится явная передача user экземпляра каждому помощнику, и он не является большим поклонником руководства each цикла. Он сморщивает нос … должен быть лучший способ сделать это.

Вложенные клетки

В ООП, когда вы начинаете проходить вокруг объектов подряд, это часто является индикатором неправильного проектирования объектов.

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

Скотт снова надевает свою белую шляпу архитектора. «Да, это звучит как хороший ООП».

Логический вывод — ввести новую ячейку для одного пользователя. Он будет жить в app / cell / user_index_detail_cell.rb .

Мы все знаем, что имя более чем нечетное и является результатом отсутствия соглашения Rails о пространстве имен. Давайте пока поговорим об этом, но помните, что в следующем посте будут представлены ячейки Trailblazer, где пространства имен и строгие соглашения делают этот вид намного приятнее:

 class UserIndexDetailCell < Cell::ViewModel def show render end private def link link_to(model.email, model) end def avatar image_tag(model.avatar) end end 

Мы удалили link и avatar из UserIndexCell (да, удалите этот код, до свидания) и переместили его в UserIndexDetailCell . Поскольку последний должен представлять только одного пользователя, мы можем смело использовать model здесь, и не нужно ничего передавать.

Вот представление в app / cell / user_index_detail / show.haml — опять же, Скотт, терпите меня. Следующий пост покажет, как это можно сделать в гораздо более упорядоченной структуре:

 %li = link = avatar 

Скотт любит это. Простые представления не могут сломаться, не так ли?

Теперь, когда мы реализовали две ячейки (по одной для страницы, по одной на каждого пользователя в списке), как нам их соединить? Простой вызов вложенных ячеек сделает свое дело , как показано в следующем представлении app / cell / user_index / show.haml :

 %h1 All Users %ul - model.each do |user| = cell("user_index_detail", user) 

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

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

В любом случае, рендеринг коллекций — это то, о чем уже думали авторы Cells.

Рендеринг коллекций

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

 %h1 All Users %ul = cell("user_index_detail", collection: model) 

Предоставляя опцию :collection , Cells сделает итерацию за вас! И, что хорошо, в следующей версии Cells 5.0 это будет иметь еще одно значительное повышение производительности благодаря большему упрощению.

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

Испытательные ячейки

Клетки — это объекты, и объекты очень легко проверить.

Теперь, с чего начать с такого количества объектов? Мы могли бы начать тестирование отдельной ячейки, просто для написания тестов. Скотт предпочитает Минитест Рспеку. Это не значит, что Скотт хочет начать очередную религиозную войну за рамки испытаний.

Клеточный тест состоит из трех этапов:

  1. Настройте среду тестирования, например, используя приборы.
  2. Вызовите фактическую ячейку.
  3. Проверьте вывод. Обычно это делается с помощью точных совпадений Капибары.

Говоря о Капибаре, чтобы правильно использовать этот драгоценный камень в Minitest, желательно включить соответствующий драгоценный камень в свой Gemfile:

  group :test do gem "minitest-rails-capybara" ... end 

В test_helper.rb некоторые помощники Capybara должны быть включены в ваш базовый класс Spec . Это избавит Скотта от ужасной головной боли или даже мигранта:

 Minitest::Spec.class_eval do include ::Capybara::DSL include ::Capybara::Assertions end 

Теперь для фактического теста. Этот тестовый файл может идти в test / cell / user_index_detail_test.rb .

 class UserCellTest < MiniTest::Spec include Cell::Testing controller UsersController let (:user) { User.create(email: "[email protected]", avatar: image_fixture) } it "renders" do html = cell(:user_index_detail, user).() html.must_have_css("li a", "[email protected]") html.must_have_css("img") end end 

Это, если вы посмотрите поближе, на самом деле просто юнит-тест. Модульный тест, в котором вы вызываете исследуемый объект и утверждаете побочные эффекты.

Побочными эффектами при рендеринге ячейки должен быть выделенный HTML, который можно легко протестировать с помощью Capybara. Скотт впечатлен.

Cell Test == Модульный тест

Интересным фактом здесь является то, что нигде не происходит никакой магии.

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

Вы должны собрать правильные данные, создать экземпляр объекта и вызвать его, а затем вы можете проверить возвращенный фрагмент HTML.

Скотт почесывает голову. Теперь он понимает, как выглядит клеточный тест. Вызов и утверждение — это все, что нужно. Однако имеет ли смысл проводить юнит-тестирование каждой маленькой ячейки? Разве не имеет смысла тестировать систему в целом, где мы только визуализируем UserIndexCell и UserIndexCell , работает ли она?

Верно, Скотт.

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

Преимущество нисходящего подхода заключается в том, что при смене внутренних компонентов вам не придется переписывать целую кучу тестов. Это кажется знакомым из «нормального» ООП тестирования? Да, это так, потому что клетки — это просто объекты.

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

 it "renders" do html = cell(:user_index, collection: [user, user2]).() html.must_have_css("li", count: 2) html.must_have_css("li a", "[email protected]") html.must_have_css("li a", "[email protected]") # .. end 

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

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

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

Что дальше?

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

В следующем посте мы обсудим некоторые экспертные функции Cells, такие как упаковка ресурсов CSS и Javascript в конвейер ресурсов Rails, просмотр наследования, кэширование и то, как Trailblazer::Cell представляет более интуитивную структуру файлов и имен.

Отлично, Скотт. Держите эти объекты в пути!