Некачественный код может привести к множеству проблем, включая трудности в сопровождении, увеличение времени на исправление ошибок и снижение производительности приложения. Инструменты для обеспечения качества кода позволяют выявлять ошибки и проверять соответствие стандартам. К таким инструментам относятся линтеры, статические анализаторы и тесты.
В этом уроке мы рассмотрим статический анализатор кода и линтер 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
для отладки кода, который позволяет отлаживать и исправлять ошибки в приложении.
Самостоятельная работа
- Создайте новый Rails-проект, если у вас его еще нет.
- Добавьте в приложение главную страницу со списком страниц и 2-3 страницы с любым содержимым.
- Добавьте в проект RuboCop и тесты на созданные страницы.
- Результат запушьте на Github
В результате у вас получится репозиторий с приложением со статическими страницами и тестами.
Дополнительные материалы
- Начинаем писать тесты (правильно)
- Тестирование и база данных. Фикстуры, фабрики, моки.
- Fixture в Rails
- Пример конфига .rubocop.yml
- Чек-лист хороших инженерных практик в компаниях
- Гем rubocop-rails
- Инструкция Rails по тестированию приложений
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.