Популярный фреймворк тестирования 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 и рассматривайте каждый тест как гарантию будущего кодовой базы.

А теперь приступайте к рефакторингу очередного запутанного набора тестов. Ваш код  —  и душевное спокойствие  —  заслуживают этого.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Jean-Michel KEULEYAN: Ruby on Rails — Write tests like a pro

Предыдущая статьяКомпонентный подход: организация навигации с помощью библиотеки Decompose. Часть 3
Следующая статьяСоздаем сайт для кинорулетки