Создаём свои теги в RSpec тестах

Это рспек.

Привет, меня зовут Леонид, и я работаю в команде, использующей Ruby on Rails, в компании Align Technology.
На работе мы используем рельсы в связке с RSpec, и сегодня я поделюсь нашим опытом о том, как облегчить себе жизнь при помощи собственных тегов в тестах.

По умолчанию, рспек в рельсах имеет несколько директорий для группировки тестов по тому, что они тестируют: модели, отдельно контроллер, вид, несколько контроллеров, маршрутизацию.

Внутри себя рспек проставляет теги (метаданные), по которым он дополняет тесты в какой-то группе вспомогательными методами и новыми ассертами. (Метаданные — просто хеш, в котором каждая папка добавляет на данный тест ключ type с названием данной директории)

Каждый тест для своего запуска получает собственный сгенерённый класс, в который, собственно, и подмешиваются DSL модули с нужными хелперами и ассертами.

Вы можете использовать свои собственные теги, если свойства, которым должен удовлетворять ваш тест, не позволяют поместить его в какую-то конкретную директорию.

Вот несколько полезных тегов, которые мы используем в нашей работе.


Контролируем работу поискового движка (на примере Sunspot)

Обычно поисковые библиотеки в рельсах интегрируются с ActiveRecord/ActiveModel классами, устанавливая туда after_save колбеки по вызову DSL метода конфигурации индексируемых полей.

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

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

spec/spec_helper.rb:

# --- :solr tag ---
# By default, Sunspot engine is disabled in any tests, with its connection stubbed.
# Add :solr tag to enable regular model indexing.
# Don't forget to run your search engine by corresponding rake task before running the suite.

config.before :suite do
  $real_solr_session = ::Sunspot.session
  ::Sunspot.session  = ::Sunspot::Rails::StubSessionProxy.new(::Sunspot.session)
end

config.before :each, solr: true do
  ::Sunspot.session  = $real_solr_session
end
config.after :each, solr: true do
  $real_solr_session = ::Sunspot.session
  ::Sunspot.session  = ::Sunspot::Rails::StubSessionProxy.new(::Sunspot.session)
end

Теперь просто пометьте ваши тесты, которым действительно нужен поисковый движок тегом : solr.
Например проверим, правильно ли работают синонимы в synonyms.txt:

spec/models/document.rb

it 'knows that tienda means store, in case someone will use \'STORE\' among spanish documents' do
  FactoryGirl.create :document, title: 'Lorem ipsum store dolor sit amet',
    searchable: true, locales: Locale.find_by(name:'es')
  Document.reindex

  Document.solr_search { fulltext 'tienda' }.hits.should have(1).document_match
  Document.solr_search { fulltext 'store' }.hits.should have(1).document_match
end


Тестируем миграции и «живые данные»: база данных как fixture.

На самом деле, для приложений с «небольшой» базой данных, ничего не мешает использовать всю базу в качестве одного из «тестовых наборов данных».

Это позволит делать ассёрты на настоящих данных, а также протестировать сложные миграции.

Итак, для тега : seeded тестов, которые начинают с «живого» БД-снапшота, нам понадобится несколько вещей. Первая — это Rake-таск, который достаёт свежий снапшот базы данных (например, снимаемый раз в сутки), и, если нужно, очищенный от важный продакшн-данных.

Второй момент такой: восстановление базы данных занимает время, поэтому давайте сначала запускать все «обычные» тесты, а потом сгруппируем тесты с «заполненной» базой, чтобы они запускались в самом конце:

spec/spec_helper.rb:

config.order_groups {|list| list.reject{|e| e.metadata[:seeded]}.shuffle(random: Random.new(config.seed)) \
                          + list.select{|e| e.metadata[:seeded]}.shuffle }

(Лучше при этом использовать обычный механизм запуска тестов в транзакции, которая в конце теста отменяется)

Заметим, что Rspec старается запускать тесты в случайном порядке, чтобы находить проблемы с состоянием, испорченным предыдущим тестом. (После этого отладка возможна с использованием того же порядка, если передать рспеку --seeded флаг с тем значением, которое он печатает при запуске) Поэтому мы следуем соглашению, и сбрасываем генератор случайных чисел на config.seed рспека.

Осталось задать самми правила для тега:

spec/spec_helper.rb:

# --- :seeded tag ---
# 1. have all :seeded tags grouped and executed at the end of the test suite
# 2. have your SQL test DB filled with fresh data from content master
# 3. then migrated from your migrations
# REQUIREMENTS:
#   A. all :seeded tests run one after another
#   B. :seeded tag is put on the top 'describe' blocks
config.before :all, seeded: true do
  unless $rspec_seeded_database
    Rails.application.load_tasks
    Rake::Task['db:restore_from_snapshot '].invoke
    Rake::Task['db:migrate'].invoke
    $rspec_seeded_database = true
    Rails.application.reload_routes!
    #Rake::Task['sunspot:reindex'].invoke
  end
end
config.after :all, seeded: true do end

Хорошо при этом в rake-таске db: restore_from_snapshot кешировать скачанный снапшот базы в папке tmp/, чтобы не загружать его каждый раз на машину разработчика.


Тестируем развернутое приложение

Ещё лучше: мы можем использовать те же самые requests тесты для проверки «живого» приложения. Разумеется, это возможно, только если они не работают с базой вообще, или читают из базы те же данные, что и в развернутом сервере.

Пишем свой тег:

spec/spec_helper.rb:

    config.before :each, also_for_smoke_test: :true do
      Capybara.run_server        = false
      Capybara.app_host          = ENV['ACCEPTANCE_URL']
      Capybara.current_driver    = :webkit
    end

Теперь запускаем только нужную часть наших тестов как скрипт smoke-тестов:

ACCEPTANCE_URL=https://your-host.com rspec -t also_for_smoke_test

А какие теги-правила используете вы?

© Habrahabr.ru