Настройка видеомагнитофона с помощью RSpec

В процессе разработки рано или поздно нам нужно сделать некоторую интеграцию со сторонним сервисом, и обычно это означает выполнение http-запросов.

Как хороший разработчик, мы добавляем спецификации в нашу кодовую базу, чтобы убедиться, что все хорошо протестировано.

Для того чтобы наш набор тестов был более быстрым и последовательным, нам нужно имитировать наши http-запросы. Простой и хороший способ добиться этого — использовать гем Webmock, который позволяет легко имитировать http-ответы на ваши запросы.

# spec/webmock_spec.rb

stub_request(:get, 'https://example.com/service').
  to_return(status: 200 body: '{"a": "abc"}')
Вход в полноэкранный режим Выход из полноэкранного режима

Пример имитации с помощью Webmock

И это хорошо работает, но если нам нужно выполнить много запросов для выполнения задачи или бизнес-логики, простое имитирование становится сложным и трудным для поддержки.

Пример: Представим, что нам нужно импортировать некоторые продукты в нашу систему, но эти продукты разделены на различные категории, и чтобы получить продукты, нам нужно получить все категории, перебрать их, чтобы получить продукты:

let(:categories_endpoint) { "https://example.com/categories" }

let(:categories_response) do
  [{id: 'category-one-id'}, {id: 'category-two-id', name: ''}].to_json
end

let(:first_category_endpoint) { "#{categories_endpoint}/category-one-id" }
let(:first_category_response) do
  {
    products_links: [
      {link: 'https://example.com/categories/categor-one-id/product-one'}
    ]
  }.to_json
end

let(:second_category_endpoint) { "#{categories_endpoint}/category-two-id" }
let(:second_category_response) do
  {
    products_links: [
      {link: 'https://example.com/categories/categor-two-id/other-product'}
    ]
  }.to_json
end

let(:first_product_endpoint) { 'https://example.com/categories/categor-one-id/product-one' }
let(:first_product_response) { read_fixture('fixtures/products/first_product.json') }
let(:second_product_endpoint) { 'https://example.com/categories/categor-two-id/other-product' }
let(:second_product_response) { read_fixture('fixtures/products/second_product.json') }


before do
  stub_request(:get, categories_endpoint).to_return(status: 200, body: categories_response)

  stub_request(:get, first_category_endpoint).to_return(status: 200, body: first_category_response)
  stub_request(:get, second_category_endpoint).to_return(status: 200, body: second_category_response)

  stub_request(:get, first_product_endpoint).to_return(status: 200, body: first_product_response)
  stub_request(:get, second_product_endpoint).to_return(status: 200, body: second_product_response)
end
Войти в полноэкранный режим Выйти из полноэкранного режима

Возможный макет для примера

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

Способ избежать этого — использовать гем VCR.

Гем VCR

VCR — это гем, который записывает http-взаимодействия в нашем тестовом наборе и позволяет воспроизводить эти взаимодействия в будущих запусках.

Использование VCR

Использование VCR довольно простое и заключается в следующем:

# Configure it

VCR.configure do |config|
  config.cassette_library_dir = "fixtures/vcr_cassettes"
  config.hook_into :webmock
end

# Uses it

it 'does something' do
  VCR.use_cassette('my_cassete') do
    expect(do_request.body).to eql({success: true}.to_json)
  end
end
Войти в полноэкранный режим Выйти из полноэкранного режима

Конфигурация проста, но использование немного раздражает, требуя от нас обернуть все HTTP-взаимодействия в блоки, что также нарушает шаблон TDD «configure, execute and assert».

VCR с RSpec

VCR обеспечивает интеграцию с RSpec, который использует метаданные RSpec для конфигурирования себя. Конфигурация, необходимая для использования этой возможности, находится здесь. Но идея этой статьи заключается в том, чтобы иметь тонкий контроль над VCR, поэтому мы не будем ее использовать.

Тонкий контроль над VCR с помощью RSpec

Давайте настроим видеомагнитофон с помощью RSpec, чтобы предоставить нам более тонкий контроль. Конфигурация выполняется следующим образом:

  • Обычная конфигурация видеомагнитофона
  • Настройте видеомагнитофон так, чтобы он не запускался на спецификациях, которые не требуются
  • Настройте видеомагнитофон с помощью RSpec shared_contexts.

Обычная конфигурация видеомагнитофона

Обычная конфигурация, как указано в документации VCR.

require 'vcr'

VCR.configure do |c|
  c.cassette_library_dir = 'spec/vcr_cassettes'
  c.hook_into :webmock
end
Вход в полноэкранный режим Выход из полноэкранного режима

Отключить VCR в спецификациях, в которых он не требуется

Это не совсем обязательно, но это хорошая практика, также это заставит наши тесты не работать, если они выполняют некоторые HTTP запросы или используют обычные макеты без проблем.

У VCR есть метод turned_off, который принимает блок кода, который должен быть выполнен без VCR. Поэтому для отключения VCR в спецификациях, где он не требуется, мы будем использовать RSpec хук around:

# specs/spec_helper.rb

RSpec.configure do |config|
  config.around do |example|

    # Just disable the VCR, the configuration for its usage
    # will be done in a shared_context
    if example.metadata[:vcr]
      example.run
    else
      VCR.turned_off { example.run }
    end
  end
end
Войти в полноэкранный режим Выйти из полноэкранного режима

Настройка разделяемого_контекста видеомагнитофона

Настроенный в RSpec shared_context позволит нам включать видеомагнитофон только тогда, когда это необходимо:

shared_context 'with vcr', vcr: true do
  around do |example|
    VCR.turn_on!

    VCR.use_cassette(cassette_name) do
      example.run
    end

    VCR.turn_off!
  end
end
Войти в полноэкранный режим Выйти из полноэкранного режима

С этим shared_context мы можем использовать его следующим образом, и http будет записан:

describe 'using vcr', vcr: true do
  # Configure the cassete name
  let(:cassete_name) { 'path/to/the/interaction' }

  it 'record the http interaction' do
    expect(do_request.body).to eql({ success: true }.to_json)
  end

  it 'reuse the same cassete here' do
    expect(do_request.headers).to include('x-custom-header' => 'abc')
  end
end

Войти в полноэкранный режим Выход из полноэкранного режима

Улучшение общего_контекста

Метод VCR use_cassete принимает множество других опций, например, record_mode. Использование shared_context и let позволяет нам настроить VCR, например, на запись новых взаимодействий в разработке, но на CI выдать ошибку:

shared_context 'with vcr', vcr: true do
  # Disable new records on CI. Most of the CI providers
  # configure environment variable called CI.
  let(:cassette_record) { ENV['CI'] ? :none : :new_episodes }

  around do |example|
    VCR.turn_on!

    VCR.use_cassette(cassette_name, { record: cassette_record }) do
      example.run
    end

    VCR.turn_off!
  end
end
Войти в полноэкранный режим Выход из полноэкранного режима

Создание специфических разделяемых_контекстов

Можно создавать специфические shared_context, которые настраивают VCR для определенных случаев. Например, представьте, что вам не нужно игнорировать заголовки для некоторых специфических запросов.

shared_context 'with vcr matching headers', vcr_matching_headers: true do
  around do |example|
    VCR.turn_on!

    VCR.use_cassette(cassette_name, { match_requests_on: [:method, :uri, :headers]}) do
      example.run
    end

    VCR.turn_off!
  end
end
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

Есть еще много опций для VCR, которые мы могли бы добавить, но эти примеры дают представление о том, как управлять VCR в нашем тестовом пакете.

Оцените статью
devanswers.ru
Добавить комментарий