Зарегистрируйтесь, чтобы продолжить обучение

Качество Ruby On Rails

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

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

Статический анализатор RuboCop

RuboCop — это статический анализатор кода для Ruby, который помогает поддерживать стиль кода и следовать соглашениям в проекте. Он основан на Ruby Style Guide и предоставляет правила (копов), которые можно настраивать в зависимости от потребностей проекта. Основные возможности RuboCop включают:

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

Установка RuboCop

Чтобы установить RuboCop для Rails, нужно добавить его в Gemfile:

group :development, :test do
  gem 'rubocop'
  gem 'rubocop-rails'
  gem 'rubocop-performance'
end

Так как в это dev-зависимость, то и добавляется Rucobop в окружение для разработки и для тест-окружения (для CI).

Дальше необходимо установить новую зависимость с помощью Bundler:

bundle install

Проверим, что RuboCop установлен:

bundle exec rubocop -v
1.69.2

Файл конфигурации

RuboCop по умолчанию поставляется со своими правилами и проверяет весь проект. Чтобы применить тонкую настройку, мы можем создать файл .rubocop.yml и добавить туда правила проверок:

---

AllCops:
  DisplayCopNames: true
  Exclude:
    - "bin/**"
    - "db/schema.rb"
    - "db/migrate/**"
    - "vendor/**/*"
    - "node_modules/**/*"
  NewCops: enable
  TargetRailsVersion: 6
  TargetRubyVersion: 3.2

Style/AsciiComments:
  Enabled: false

Style/ClassAndModuleChildren:
  Enabled: false

Style/Documentation:
  Enabled: false

Style/IfUnlessModifier:
  Enabled: false

# ...

Ранее вместе с RuboCop мы добавили гемы rubocop-rails и rubocop-performance - это наборы правил для Rails-приложений. Чтобы их включить нужно их добавить в .rubocop.yml:

require:
  - rubocop-performance
  - rubocop-rails

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

RuboCop установлен и настроен, теперь необходимо проверить проект на ошибки и соответствие стайл гайду.

Проверка кода

Если мы запустим RuboCop первый раз, мы увидим множество ошибок. Точное их количество будет в конце

bundle exec rubocop
Inspecting 33 files
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Offenses:

Gemfile:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
source "https://rubygems.org"
...
33 files inspected, 166 offenses detected, 155 offenses autocorrectable

You can opt out of this message by adding the following to your config (see https://docs.rubocop.org/rubocop/extensions.html#extension-suggestions for more options):
  AllCops:
    SuggestExtensions: false

Вывод команды bundle exec rubocop показывает, что было проверено 33 файла, и обнаружено 166 недочетов (offenses). Все недочеты помечены буквой "C", что указывает на то, что они являются проблемами стиля (Correctable) и их можно исправить автоматически.

RuboCop предоставляет возможность автоматической корректировки кода. Для этого используются две основные опции:

bundle exec rubocop -a
bundle exec rubocop -A
  • Опция -a исправляет только те недочеты, которые не могут повредить бизнес-логике (например, форматирование кода).
  • Опция -A может вносить изменения, которые потенциально могут повлиять на логику приложения. Но ее можно использовать без опасений на новом проекте, в котором еще нет никакой логики.
bundle exec rubocop -A                                                                                                                 1 ↵
Inspecting 33 files
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Offenses:
# ...
test/test_helper.rb:3:9: C: [Corrected] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
require "rails/test_help"
        ^^^^^^^^^^^^^^^^^

33 files inspected, 225 offenses detected, 215 offenses corrected

You can opt out of this message by adding the following to your config (see https://docs.rubocop.org/rubocop/extensions.html#extension-suggestions for more options):
  AllCops:
    SuggestExtensions: false

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

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

Rails предоставляет мощные инструменты для написания и выполнения тестов. Тесты обычно располагаются в директории test/, где они разделены по директориям:

  • test/models/ — для тестов моделей.
  • test/controllers/ — для тестов контроллеров.
  • test/integration/ — для интеграционных тестов.
  • test/system/ — для системных тестов.

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

Пример тестов контроллера резюме в Hexlet CV:

# https://github.com/Hexlet/hexlet-cv/blob/592468dbe24a33d1d12de779d3fcdb2fb030311f/test/controllers/web/resumes_controller_test.rb
class Web::ResumesControllerTest < ActionDispatch::IntegrationTest
  setup do
    @resume = resumes(:one)
  end

  test '#show' do
    get resume_path(@resume, locale: I18n.locale)

    @resume.reload
    assert_response :success
    assert { @resume.impressions_count == 1 }
  end

  test '#show from boot' do
    @resume.destroy
    get resume_path(@resume, locale: I18n.locale), headers: { 'User-Agent': 'Bot' }
    assert_redirected_to root_path
  end

  test '#index rss' do
    get resumes_path(format: :rss)
    assert_response :success
  end

  test '#user anonimus' do
    user = users(:without_last_name_and_first_name)
    sign_in(user)

    get resume_path(@resume, locale: I18n.locale)
    assert_redirected_to edit_account_profile_path
  end

  test '#user is author' do
    user = @resume.user
    sign_in(user)

    get resume_path(@resume, locale: I18n.locale)

    @resume.reload
    assert_response :success
    assert { @resume.impressions_count.zero? }
  end

  test '#user is not author' do
    user = users(:full)
    sign_in(user)

    get resume_path(@resume, locale: I18n.locale)

    @resume.reload
    assert_response :success
    assert { @resume.impressions_count == 1 }
  end
end

В этих тестах используется фреймворк тестирования Minitest с гемом power-assert.

Фикстуры

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

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

DEFAULTS: &DEFAULTS
  first_name: $LABEL
  last_name: Last
  encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>
  locale: <%= I18n.locale %>
  confirmed_at: <%= Time.current %>
  resume_mail_enabled: true
  role: :user
  state: permitted

full:
  <<: *DEFAULTS
  email: $LABEL@email.com
  about: PR-специалист Hexlet
  role: admin

Фикстуры автоматически загружаются в базу данных перед выполнением тестов. Это происходит благодаря методу fixtures() в тестах, который указывает, какие фикстуры нужно загрузить.

# test/test_helper.rb

module ActiveSupport
  class TestCase
    parallelize(workers: :number_of_processors)
    # Загружаем все фикстуры
    fixtures :all
  end
end

В тестах можно обращаться к загруженным фикстурам по их идентификаторам:

  test '#user is not author' do
    # обращаемся к модели User с id = full
    user = users(:full)
    sign_in(user)

    get resume_path(@resume, locale: I18n.locale)

    @resume.reload
    assert_response :success
    assert { @resume.impressions_count == 1 }
  end

Фикстуры позволяют легко создавать и управлять тестовыми данными. Особенно там, где требуется сложное состояние БД. Они обеспечивают консистентность данных между тестами. Фикстуры можно использовать с гемами faker или factory_bot_rails для того, чтобы упростить создание фикстур там, где не нужны конкретные значения.

Транзакционные тесты

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

Каждый тест выполняется в своей транзакции, что предотвращает "загрязнение" базы данных. Это особенно полезно, когда тесты изменяют данные, так как изменения не сохраняются после завершения теста. В Rails при использовании ActiveSupport::TestCase транзакционные тесты включены по умолчанию.

Пример транзакционного теста:

  test '#create and to draft' do
    attrs = {
      name: 'Ruby developer'
    }

    params = {
      hide: true,
      resume: attrs
    }

    post(account_resumes_path, params:)
    assert_response :redirect

    resume = Resume.find_by(attrs)
    assert { resume.not_evaluated? }
  end

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

Когда мы вызываем генератор для создания контроллера, то создается также класс для тестирования этого контроллера. Вызовем генератор для создания контроллера:

bin/rails g controller pages
      create  app/controllers/pages_controller.rb
      invoke  erb
      create    app/views/pages
      invoke  test_unit
      create    test/controllers/pages_controller_test.rb
      invoke  helper
      create    app/helpers/pages_helper.rb
      invoke    test_unit

Получили пустой контроллер:

class PagesController < ApplicationController
end

И тестовый класс без методов

class PagesControllerTest < ActionDispatch::IntegrationTest
  # test "the truth" do
  #   assert true
  # end
end

Добавим обработчик в config/routes.rb

Rails.application.routes.draw do
  # Используем сразу же ресурсный роутинг
  resources :pages, only: [:index]
end

И добавим тест на добавленный роут


class PagesControllerTest < ActionDispatch::IntegrationTest
  test '#index' do
    get pages_url

    assert_response :success
  end
end

В контроллер добавим экшен:

class PagesController < ApplicationController
  def index; end
end

И шаблон app/views/pages/index.html.erb, который будет выводиться экшеном:

<h1>Pages catalog</h1>
<p>Find me in app/views/pages/index.html.erb</p>

Теперь мы готовы запустить тесты и проверить, что все работает. Запустить тесты можно командой bin/rails test:

bin/rails test                                                                                                                         1 ↵
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 24561

# Running:

.

Finished in 0.322993s, 3.0960 runs/s, 3.0960 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Запуск тестов завершился успешно. Если на каком-то из этапов возникла бы ошибка, то тесты бы об этом сообщили. Например так выглядит ошибка, если нет экшена и шаблона:

bin/rails test                                                                                                                       130 ↵
Running 1 tests in a single process (parallelization threshold is 50)
Run options: --seed 25906

# Running:

F

Failure:
PagesControllerTest#test_#index [test/controllers/pages_controller_test.rb:11]:
Expected response to be a <2XX: success>, but was a <404: Not Found>


bin/rails test test/controllers/pages_controller_test.rb:8


Finished in 0.652640s, 1.5322 runs/s, 1.5322 assertions/s.
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

binding.irb

В Ruby и Rails есть инструмент для отладки кода - binding.irb, который позволяет вам вставлять интерактивную консоль в любое место вашего кода. Это может быть особенно полезно для диагностики проблем, анализа состояния переменных и выполнения произвольного кода в контексте текущего выполнения программы.

Чтобы использовать irb, нужно вызвать binding.irb внутри кода:

class UsersController < ApplicationController
  def index
    @users = User.all
    binding.irb
  end
end

Использовать binding.irb можно даже во время выполнения тестов

bin/rails test
Running 7 tests in a single process (parallelization threshold is 50)
Run options: --seed 57184

# Running:

.....
app/controllers/users_controller.rb @ line 7 :

     3:
     4:   # GET /users or /users.json
     5:   def index
     6:     @users = User.all
 =>  7:     binding.irb
     8:   end
     9:
    10:   # GET /users/1 or /users/1.json
    11:   def show
    12:   end

irb(#<UsersController:0x00007526f...):001>

Заключение

В ходе урока мы познакомились с RuboCop — статическим анализатором кода для Ruby, который помогает поддерживать стиль и стандарты кода в проектах. Мы рассмотрели процесс установки и настройки RuboCop, включая создание конфигурационного файла .rubocop.yml для настройки правил проверки. Также мы изучили, как использовать RuboCop для анализа кода и автоматической корректировки недочетов.

Мы обсудили основы тестирования в Rails, включая использование фикстур и транзакционных тестов, а также написание простых тестов для контроллеров.

Рассмотрели инструмент binding.irb для отладки кода, который позволяет отлаживать и исправлять ошибки в приложении.


Самостоятельная работа

  1. Создайте новый Rails-проект, если у вас его еще нет.
  2. Добавьте в приложение главную страницу со списком страниц и 2-3 страницы с любым содержимым.
  3. Добавьте в проект RuboCop и тесты на созданные страницы.
  4. Результат запушьте на Github

В результате у вас получится репозиторий с приложением со статическими страницами и тестами.


Дополнительные материалы

  1. Начинаем писать тесты (правильно)
  2. Тестирование и база данных. Фикстуры, фабрики, моки.
  3. Fixture в Rails
  4. Пример конфига .rubocop.yml
  5. Чек-лист хороших инженерных практик в компаниях
  6. Гем rubocop-rails
  7. Инструкция Rails по тестированию приложений

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff