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