Тестирование Rails приложения с Turbo Frames

Turbo Frames позволяет динамически изменять отдельные области страницы без использования JavaScript.

Практика применения Turbo Frames на текущий момент еще не получила достаточно широкого распространения. Это связано с достаточной "новизной" данного инструмента, его доработками и развитием.

Тем не менее Turbo Frames успешно справляется с задачами по созданию динамических страниц в Rails приложениях без использования дополнительных JS библиотек.

При применении Turbo Frames в приложении возникает задача тестирования его работоспособности, в том числе с различными системами для автоматизации процесса тестирования.

Поскольку Turbo Frames использует уже включенные в последние версии Rails 7 библиотечные JavaScript, для тестирования будем использовать feature тесты на основе capybara и rspec, как одну из самых распространенных конструкций для приложений Rails.

Тестовая система

Тестовая система состоит из

  • rspec

  • capybara

  • selenium-webdriver

Gemfile

Добавляем перечисленные gem в Gemfile

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem 'rspec-rails'
  gem 'capybara'
  gem 'selenium-webdriver'
end

Будем использовать chrome_headless драйвер из набора selenium-webdriver, по проведенным тестам он показывает почти двукратное преимущество в скорости по сравнению с драйвером, используемым capybara по умолчанию (selenium)

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

Конфигурация драйвера

Создаем отдельный файл конфигурации драйвера spec/support/capybara.rb и добавляем его в spec/rails_helper.rb

require 'support/capybara'

# spec/support/capybara.rb

# Setup chrome headless driver
Capybara.server = :puma, { Silent: true }

Capybara.register_driver :chrome_headless do |app|
  options = Selenium::WebDriver::Chrome::Options.new

  options.add_argument('--headless')
  options.add_argument('--no-sandbox')
  options.add_argument('--disable-dev-shm-usage')
  options.add_argument('--window-size=1400,1400')

  Capybara::Selenium::Driver.new(app, browser: :chrome, options:)
end

Capybara.javascript_driver = :chrome_headless

# Setup rspec
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :rack_test
  end

  config.before(:each, js: true, type: :system) do
    driven_by :chrome_headless
  end
end

Приведена работающая конфигурация настроек для драйвера, найденная на просторах сети после ряда экспериментов. Проверена на работоспособность для различного оборудовании с операционными системами Mac OS и Linux.

Тестируемый код

Используя Turbo Frames, рекомендуется для всех элементов, которые планируется использовать при тестировании, указывать id: в HTML коде (.html.erb)

Для использования Turbo Frames id является необходимым параметром, для остальных элементов - button, link, input и т.д. - его желательно указать. Это сильно облегчает процесс написания кода для тестирования при поиске элементов по CSS(path) селектору.

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

    <%= turbo_frame_tag 'errors' do %>
      <%= render 'shared/error_messages', object: @classifier %>
    <% end %>
    <%= turbo_frame_tag 'notice' do %>
      <%= render 'shared/notice', notice: %>
    <% end %>

И код, который отображает функциональную работающую часть представления.

    <%= turbo_frame_tag 'classifiers' do %>
      <div class='box py-0'>
        <%= render 'form', frame_id: 'new', classifier: @classifier %>
      </div>
      <div class='box py-0'>
        <div class="column is-half">
          <div class='has-text-weight-bold'><%= t('classifier.title') %><sup>*</sup></div>
          <span class='is-size-7'><sup>*</sup><%= t('classifier.press_open')%></span>
        </div>
      </div>
      <%= render @classifiers %>
    <% end %>

Приведенный код выполняет следующие задачи:

  • Создание элемента классификатора и его отображение в списке

  • Отображение списка элементов с учетом добавления в том же представлении без выполнения полного render

  • In-line редактирование добавленного элемента с отображением результатов.

  • Удаление элемента и отображение результатов.

Такое же поведение страницы можно получить, используя JavaScript для всех перечисленных действий. Здесь мы его не используем.

При этом последний partial в вышеприведенном коде заключает каждую запись в turbo_frame_tag и присваивает ей уникальный id для реализации in-line редактирования и удаления элементов списка.

<%= turbo_frame_tag dom_id(classifier), class: 'box my-0 py-0' do %>
  <div class='tile'>
    <div class="column is-half">
      <%= link_to classifier.tree_path, classifiers_path(current_path: classifier.tree_path), target: '_top', class: 'navbar-item' %>
    </div>
    <div class="column is-one-fifth">
      <%= button_to edit_classifier_path(classifier), class: 'button is-link is-inverted', method: 'get', data: { tooltip: t('classifier.tooltip_edit') }, form: { id: edit_classifier_path(classifier) }  do %>
        <%= image_tag 'edit_icon', size: '20x20' %>
      <% end %>
    </div>
    <div class="column is-one-fifth">
      <%= button_to classifier_path(classifier), method: :delete, class: 'button is-link is-inverted', data: { tooltip: t('classifier.tooltip_delete'), turbo_confirm: t('classifier.confirm') }, form: { id: "#{edit_classifier_path(classifier)}/delete" } do %>
        <%= image_tag 'trash', size: '20x20' %>
      <% end %>
    </div>
  </div>
  <hr class='py-0 my-0'>
<% end %>

Тесты

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

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

В руководстве по применению методов поиска указано: Module: Capybara::Node::Finders

If the driver is capable of executing JavaScript, this method will wait for a set amount of time and continuously retry finding the element until either the element is found or the time expires. The length of time this method will wait is controlled through default_max_wait_time

(default_max_wait_time - параметр конфигурации драйвера capybara, по умолчанию равен 2 сек. и может быть переопределен)

Методы, которые можно использовать при написании теста.

Capybara::Node::Finders.instance_methods
=> [:find_by_id, :find_field, :find_link, :find, :first, :all, :sibling, :ancestor, :find_all, :find_button]

Рассмотрим пример тестирования создания записи при использовании вышеописанной формы.

Указание js: true - для использования capybara javascript драйвера, без этого будет использоваться стандартный драйвер, что в нашем случае не корректно.

RSpec.describe 'Создание Классификатора', js: true do

  context 'когда введены корректные данные' do
    before do
      visit classifiers_path
      find('turbo-frame[id=classifiers]')
      fill_in :classifier_tree_path, with: 'Test_path'
      click_on t('classifier.commit')
      find('turbo-frame[id=classifiers]')
    end

    it 'ожидается создание' do
      expect(Classifier.count).to eq 1
    end
  end
...
end

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

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

Это поведение связано со скоростью выполнения javascript на конкретном оборудовании в конкретный момент. Для того, чтобы тест работал всегда, применяется метод поиска требуемого элемента turbo-frame (напоминаю про желательность указания id для всех тестируемых элементов), который "ждет" его появления на странице.

Логика работы теста

Рассмотрим более подробно логику работы теста.

  • visit classifiers_path - загружаем стартовую страницу.

  • find('turbo-frame[id=classifiers]') - ищем загруженную часть с turbo-frame - в противном случае поля для заполнения еще не будет и возникнет ошибка, говорящая о том, что элемент с таким css не найден.

  • fill_in :classifier_tree_path, with: 'Test_path' - заполняем поле ввода

  • click_on t('classifier.commit') - перед этим нажатием на кнопку ожидание не требуется, элемент входит в turbo-frame и был отображен в момент, когда был создан сам turbo-frame.

  • find('turbo-frame[id=classifiers]') - ожидаем "возвращения" turbo-frame после commit - проверка создания записи завершается с ошибкой, что записи нет (результат 0), так как форма еще не отработала после нажатия.

Дальнейшие тестовые проверки формируются по аналогичной логике.

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

В качестве примера можно привести часть теста для проверки ошибок, где мы убеждаемся, что необходимый для проверки turbo-frame присутствует.

  context 'когда данные не корректны' do
    let(:tree_path_error) { I18n.t('activerecord.errors.models.classifier.attributes.tree_path.format') }

    before do
      fill_in :classifier_tree_path, with: 'Test path'
      click_on t('classifier.commit')
    end

    it 'ожидается получение ошибок' do
      expect(page.find('turbo-frame[id=errors]')).to have_text(tree_path_error)
    end
  end

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

expect(page.find('turbo-frame[id=errors]')).to have_text(tree_path_error)

и указываем id именно интересующего нас turbo-frame

Еще один момент, на котором хочется акцентировать внимание.

...
 context 'когда редактируем каталог/уровень' do
    it 'ожидается отображение отредактированной версии' do
      fill_in :classifier_tree_path, with: 'Test_path'
      click_on t('classifier.commit')
      find('turbo-frame[id=classifiers]')
      visit classifiers_path
      find('turbo-frame[id^=classifier_]')
      id = Classifier.last.id
      frame = find("turbo-frame[id^=classifier_#{id}]")
      form = find_by_id(edit_classifier_path(id))
      # within используется для того, чтобы найти кнопки в turbo-frame
      within form do
        click_button
        frame.fill_in :classifier_tree_path, with: 'Test_path_new'
      end
      within frame do
        click_button
      end
      # Ждем отображения страницы, ищем turbo-frame и внутри выполняем проверки.
      expect(page.find("turbo-frame[id^=classifier_#{id}]")).to have_text('Test_path_new', count: 1).and \
        have_link('Test_path_new', count: 1)
      # Проверяем что измененный путь сохранен
      expect(Classifier.find(id).tree_path).to eq 'Test_path_new'
    end
...

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

      form = find_by_id(edit_classifier_path(id))
      # within используется для того, чтобы найти кнопки в turbo-frame
      within form do
        click_button
        frame.fill_in :classifier_tree_path, with: 'Test_path_new'
      end

Краткие выводы

  • Для тестирования работы страниц с применением turbo-frame используем capybara

  • Перед выполнением любых действий (проверка, заполнение полей, нажатие на управляющие элементы и т.д.) необходимо с помощью методов capybara убедиться, что требуемый turbo-frame уже отобразился на странице.

  • Использование драйвера selenium_chrome_headless позволяет тестам выполняться быстрее при прочих равных условиях в среднем в 1,5-2 раза.

Комментарии (0)