Блог Половнёва

Проблемы с subject

subjectхелпер в RSpec для однострочных тестов. Бывает неявным и явным.

# Неявный subject
RSpec.describe User do
  # subject — User.new (described_class.new)
  it { is_expected.to validate_presence_of(:name) }

  # Такой тест разворачивается в:
  it "..." do
    expect(User.new).to validate_presence_of(:name)
  end
end

# Явный subject
RSpec.describe User do
  subject { described_class.new(role: "admin") }

  it { is_expected.to be_super_admin }
end

В однострочниках

subject здорово сокращает тесты валидаций и ассоциаций в Рельсах с Shoulda Matchers:

RSpec.describe ActivityLog do
  it { is_expected.to validate_presence_of(:user_name) }
  it { is_expected.to validate_presence_of(:event) }
  it { is_expected.to validate_presence_of(:ip) }

  it { is_expected.to belong_to(:user) }
end

Но излишняя любовь к однострочным тестам порождает чудовищ:

RSpec.describe Alarm do
  describe "#snooze" do
    subject { described_class.new(at: time) }

    it { expect { subject.snooze }.to change { subject.at }.by(9.minutes)
  end
end

Не помогает и именованный subject:

RSpec.describe Alarm do
  describe "#snooze" do
    subject(:alarm) { described_class.new(at: time) }

    it { expect { alarm.snooze }.to change { alarm.at }.by(9.minutes)
  end
end

Такие тесты тяжело воспринимать. Чтобы понять, что snooze делает, придется прочитать и скомпилировать, расшифровать проверку в голове.

it { expect(...) } — плохо

Чтобы исправить ситуацию, разверните тест и добавьте описание:

# Ясно, что #snooze передвинет будильник на 9 минут вперед,
# без чтения проверки
it "pauses alarm for next 9 minutes" do
  expect {
    subject.snooze
  }.to change {
    subject.at
  }.by(9.minutes)
end
subject для проверок валидаций и ассоциаций в Рельсах — хорошо

В начале спеки

Когда subject объявляют в начале спеки и используют в проверках, спека получает глобальную переменную и становится запутанной. Чтобы понять, что проверяет тест, приходится возвращаться в начало:

RSpec.describe Post do
  let(:author) { build(:author, name: "Melanie C") }
  subject { build(:post, author: author, title: "Awesome News") }

  describe "#slug" do
    it "generates slug from title" do
      expect(subject.slug).to eq "awesome-news"
      # Чтобы понять, почему slug именно такой,
      # придется вернуться в начало спеки и увидеть
      # title у поста
    end
  end

  describe "#author_name" do
    subject { post.author_name }

    context "with author" do
      it { is_expected.to eq "Melanie C" }
      # Чтобы понять, откуда взялась Мелани Си,
      # придется вернуться в начало спеки (и в 90-е, конечно)
    end

    context "without author" do
      let(:author) { nil }

      it { is_expected.to eq "Annoymouse" }
      # Чтобы понять, почему здесь Annoymouse, что
      # и где меняет author = nil,
      # придется вернуться в начало спеки
      # и пройтись по всем describe/let/context
    end
  end
end

Проблема в тестах выше в том, что информация важная для восприятия проверки далеко от нее. Читатель превращается в Шерлока Холмса и тратит время впустую, расследуя, что скрывает subject.

Глобальный subject — плохо

Чтобы поправить это, перенесите настройку ближе к проверке и оставьте только значащую информацию:

# Хорошо: информация нужная для восприятия проверки на месте
describe "#slug" do
  it "generates slug from title" do
    # Нам нет дела до автора, а вот title важен
    post = build(:post, title: "Awesome News")

    expect(post.slug).to eq "awesome-news"
  end
end
# Так себе: информация стала ближе к проверке,
# но чтобы понять смысл проверки,
# придется ее «скомпилировать» в голове
describe "#author_name" do
  context "with author" do
    let(:author) { build(:author, name: "Melanie C") }
    let(:post) { build(:post, author: author) }

    subject { post.author_name }

    it { is_expected.to eq "Melanie C" }
  end
end
# Лучше: информация стала ближе к проверке.
# Описание теста ясно дает понять, что проверяем
describe "#author_name" do
  context "with author" do
    let(:author) { build(:author, name: "Melanie C") }
    let(:post) { build(:post, author: author) }

    it "returns author's name" do
      expect(post.author_name).to eq "Melanie C"
    end
  end
end
# Хорошо: информация нужная для восприятия проверки на месте
describe "#author_name" do
  context "with author" do
    it "returns author's name" do
      author = build(:author, name: "Melanie C")
      post = build(:post, author: author)

      expect(post.author_name).to eq "Melanie C"
    end
  end
end
Держать информацию нужную для понимания проверки рядом с ней — хорошо

Дополнительное чтение

P. S. Ещё больше постов о программировании, тестах и культуре разработки у меня в Телеграме.