По хардкору: дублеры, моки, стабы

Сегодня о тестах. Пост для тех, кто знаком с RSpec, но не понимает, что такое «мокать» и «застабить». Коротко, по делу и с примерами.

Дублер (test double)

Объект-каскадер, подменяющий реальный объект системы во время тестов:

describe NotificationsController do
  # NotificationsController загружает последние уведомления
  # со стороннего сервиса по HTTP
  # с помощью NotificationsDatasource.
  let(:datasource) do
    double(:datasource, as_json: { notifications: [] })
  end

  before do
    # Подменяем реальный NotificationsDatasource дублером,
    # чтобы не зависеть от внешнего сервиса в тестах:
    allow(NotificationsDatasource)
      .to receive(:new)
      .and_return(datasource)
  end

  describe "#index" do
    it "wraps notifications in 'data' key" do
      get :index, format: :json

      expect(json_response["data"].keys)
        .to have_key "notifications"
    end
  end
end

Стаб (stub)

Заглушка для метода или объекта, возвращающая заданное значение:

context "when attachment file is too large to email" do
  let(:max_file_size) { Attachment::MAX_FILE_SIZE }

  before do
    allow(attachment)
      .to receive(:file_size)
      .and_return(max_file_size + 1)
  end

  it "raises 'file is too large' error" do
    # ...
  end
end

Внимательный читатель со звездочкой уже заметил, что и в предыдущем примере с NotificationsController был стаб. Все верно: стаб — это дублер с зашитыми ответами.

Мок (mock)

Стаб с ожиданиями, которые RSpec проверит в конце теста:

context "when cloning process cannot be performed" do
  before do
    allow(doctor).to receive(:clone) { raise "can't clone" } # стаб
  end

  it "notifies airbrake" do
    expect(Airbrake).to receive(:notify) # мок
    # Rspec застабит `Airbrake.notify`
    # и в конце этого `it do...end` блока
    # проверит, был ли он вызван.
    # Если вызова не было — ошибка и красный тест.
    #
    # Когда на собеседовании спросят, чем
    # отличается мок от стаба, отвечайте:
    # «Только мок может завалить тест».

    clone_poor_dolly
  end
end

Моки меняют порядок фаз в тесте. Вместо «Подготовка — Испытание — Проверка» получается «Проверка+Подготовка — Испытание». Если вам, как и мне, тяжело такое читать, используйте стаб с проверкой:

# мок
it "notifies airbrake" do
  expect(Airbrake).to receive(:notify) # проверка + настройка

  clone_poor_dolly # испытание
end

# стаб + проверка
it "notifies airbrake" do
  allow(Airbrake).to receive(:notify) # настройка

  clone_poor_dolly # испытание

  expect(Airbrake).to have_received(:notify) # проверка
end

Дублеры, моки и стабы привязывают тесты к интерфейсу используемого объекта, а реальные объекты — к их реализации. Чтобы узнать об этой дилемме больше и понять, стоит ли вам мокать-стабить все подряд, почитайте: