Популярный фреймворк тестирования RSpec многие любят… и иногда до ненависти. То есть любят тесты, но вот писать их — совсем другая история.
Тестирование — это как есть овощи: полезно, но не всегда вкусно. Хорошо написанный набор тестов — основа стабильного, сопровождаемого приложения. Как же в RSpec пишут оптимизированные, быстрые и точные тесты? Узнаем это на простых, но эффективных примерах, беря на вооружение хорошие практики и избегая типичных ошибок, из-за которых тесты превращаются в катастрофу.
Основы хорошего теста
Сначала обозначим, какой тест — «хороший»:
1.Читаемый: что делается тестом, понятно сразу.
2. Сопровождаемый: при изменении требований легко обновляется.
3. Изолированный: тестируется что-то одно и только одно, зависимости от других тестов избегаются.
4. Быстрый: медленных тестов ждать некогда, разве что во время перерыва на кофе, да и то…
Применим эти принципы на практике.
Пример 1. Правильное тестирование моделей
Начнем с простого — тестирования модели. Приведем примеры того, как писать тест модели User с проверкой наличия сообщения.
Плохой пример:
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
it 'is not valid without an email' do
user = User.new
expect(user.valid?).to be(false)
expect(user.errors[:email]).to include("can't be blank")
end
it 'is valid with an email' do
user = User.new(email: '[email protected]')
expect(user.valid?).to be(true)
end
end
Что здесь не так?
- Дублирование: повторение
User.newв каждом тесте. - Отсутствие контекста: описания тестов расплывчаты.
- Плохое структурирование: все свалено вместе в двух широких тестовых сценариях.
Хороший пример:
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
subject { User.new(email: email) }
context 'when email is present' do
let(:email) { '[email protected]' }
it 'is valid' do
expect(subject).to be_valid
end
end
context 'when email is not present' do
let(:email) { nil }
it 'is not valid' do
expect(subject).not_to be_valid
expect(subject.errors[:email]).to include("can't be blank")
end
end
end
Что здесь оптимальнее?
- Использование
let: извлекаются определения переменных, дублирование сокращается.letздесь применяется без восклицательного знака, потому что «ленивый» — вычисляется только при вызове, сохраняя эффективность тестов.let!же вычисляется немедленно, лишний раз замедляя тесты, при том что переменная не всегда нужна. Главное — применятьletтолько для того, что нужно при запуске теста, аlet!— для ситуаций, когда явно требуется выполнить настройку перед каждым примером. - Контекстные блоки: имеется четкая структура, благодаря которой тесты понятны и удобны для восприятия. Важно, чтобы в тексте блока
contextточно отражалось то, что внутри него, поэтому всегда следите за соответствием описания содержимому блока. Так тесты остаются честными и понятными. - Изолированные тесты: каждым тестом проверяется один аспект без лишних зависимостей.
Пример 2. Вместо имитации базы данных — фабрики
FactoryBot — понятный, простой способ создания данных в Rails. С ним тесты делаются реалистичными, их настройка не загромождается.
Плохой пример:
it 'returns the user with the highest score' do
user1 = User.create!(name: 'Alice', score: 50)
user2 = User.create!(name: 'Bob', score: 75)
expect(User.highest_score).to eq(user2)
end
Что здесь не так?
- «Сырые» вызовы
.create!: объекты, создаваемые непосредственно в тестах, сложнее читаются и управляются. - Жестко заданные значения: при тестировании разных сценариев значения изменить сложнее.
Хороший пример:
it 'returns the user with the highest score' do
user1 = create(:user, name: 'Alice', score: 50)
user2 = create(:user, name: 'Bob', score: 75)
expect(User.highest_score).to eq(user2)
end
Почему он оптимальнее?
- Используются фабрики: с
FactoryBotнастройка тестов сохраняется чистой и сопровождаемой. - Гибкость: фабрики легко меняются при изменении модели, ими генерируются разнообразные тестовые данные, тесты при этом не загромождаются.
Пример 3. Соблюдение в тестах принципа DRY
Тесты ничем так не замедляются, как дублированием. Снова и снова пишете один и тот же код настройки? Значит, пришло время рефакторинга.
Плохой пример:
it 'does something' do
user = create(:user)
order = create(:order, user: user)
# ... тестовый код ...
end
it 'does something else' do
user = create(:user)
order = create(:order, user: user)
# ... еще тестовый код ...
end
Что здесь не так?
- Дублирование:
userиorderопределяются в тестах многократно, что чревато избыточным кодом, тесты сложнее для восприятия и сопровождения.
Хороший пример:
let(:user) { create(:user) }
let(:order) { create(:order, user: user) }
it 'does something' do
# ... тестовый код ...
end
it 'does something else' do
# ... еще тестовый код ...
end
Почему он оптимальнее?
- Меньше дублирования: блоками
letобщие переменные определяются лишь раз. - Выше сопровождаемость: если что-то меняется, обновляется в одном месте.
Заключение
Написание хороших тестов с RSpec не ограничивается примерами с зелеными галочками. Это создание четких, сопровождаемых тестов, которые являются бесценным ключом к пониманию кода — для внешних участников проекта и для вас самих, когда спустя месяцы вернетесь к этому коду.
Хорошие тесты — это как страховочные сети, куда попадаются те скрытые баги, которые проскакивают при настройке или рефакторинге кода. Эти тесты — ваш верный помощник, который предупредит, когда что-то сломается, прежде чем это что-то окажется в продакшене.
Тестирование сродни инвестициям: вначале требуется немного времени, но эти вложения окупаются с лихвой, когда по мере развития приложения код остается надежным и предсказуемым.
Придерживайтесь контекстов, в которых точно отражается то, что тестируется, разумно применяйте let для соблюдения в коде принципа DRY и рассматривайте каждый тест как гарантию будущего кодовой базы.
А теперь приступайте к рефакторингу очередного запутанного набора тестов. Ваш код — и душевное спокойствие — заслуживают этого.
Читайте также:
- Ruby on Rails 7: важные рекомендации для высококачественного кода
- Ruby: рефакторинг без лишних сложностей
- 14 вопросов по валидациям на Ruby on Rails
Читайте нас в Telegram, VK и Дзен
Перевод статьи Jean-Michel KEULEYAN: Ruby on Rails — Write tests like a pro





