AnyFixture
Фикстуры в Rails работает ни в пример быстрее фабрик, однако за скорость приходится платить меньшей гибкостью и временем на поддержку. Прежде всего это связано с тем, что фикстуры используют декларативный подход (YAML файлы с описанием данных).
Мы предлагаем альтернативный подход к генерации глобального состояния для тестов – AnyFixture.
AnyFixture позволяет использовать любой блок кода в качестве фикстуры. При этом не нужно думать о чистоте базы данных — AnyFixture позаботится о зачистке затронутых таблиц после выполнения всех тестов.
Рассмотрим пример:
# Мы рекомендуем использовать контексты для подключения фикстур
RSpec.shared_context "account" do
# Внимание! Вызов AnyFixture должен происходит вне транзакций;
# для этого мы используем before(:all)
before(:all) do
# Имя ("account") — это уникальный идентификатор фикстуры
@account = TestProf::AnyFixture.register(:account) do
# Внутри блока вы можете гененировать данные любым удобным вам способом.
# Например, с помощью FactoryBot
FactoryBot.create(:account)
# или Fabrication
Fabricate(:account)
# или ActiveRecord
Account.create!(name: "test")
end
end
# Повторный вызов .register вернёт ранее созданный объект
let(:account) { TestProf::AnyFixture.register(:account) }
# Рекомендуется «пересоздавать» объект для избежания утечки состояния в тестах
let(:account) { Account.find(TestProf::AnyFixture.register(:account).id) }
end
RSpec.configure do |config|
config.include_context "account", account: true
end
# Активировать фикстуру можно, добави тег
describe UsersController, :account do
# ...
end
# Этот тест будет использовать тот же объект аккаунта
describe PostsController, :account do
# ...
endИнструкция
RSpec
В вашем spec_helper.rb (или rails_helper.rb при наличии) добавьте строчку:
require "test_prof/recipes/rspec/any_fixture"Теперь модуль TestProf::AnyFixture доступен в ваших тестах.
Minitest
При использовании с Minitest вам необходимо вручную добавить код для зачистки базы после выполнения тестов. Например:
# test_helper.rb
require "test_prof/any_fixture"
at_exit { TestProf::AnyFixture.clean }DSL
AnyFixture предоставляет дополнительный DSL (синтаксический сахар) для более лаконичного определения фикстур:
require "test_prof/any_fixture/dsl"
# Подключаем DSL с помощью refinements
using TestProf::AnyFixture::DSL
# Теперь вам доступен метод `fixture` (псевдоним для `TestProf::AnyFixture.register`)
before(:all) { fixture(:account) { create(:account) } }
# Он также может быть использован для доступа к уже созданному объекту
let(:account) { fixture(:account) }ActiveRecord#refind
Для пересоздания объектов ActiveRecord (и избежания утечек состояния) вы можете использовать метод #refind, доступный через refinements:
# вместо
let(:account) { Account.find(fixture(:account).id) }
# загружаем refinement
require "test_prof/ext/active_record_refind"
using TestProf::Ext::ActiveRecordRefind
let(:account) { fixture(:account).refind }Временный откат фикстур
Некоторые тесты могут требовать чистой базы данных, а значит их выполнение вместе с AnyFixture может приводить к ошибкам.
Вы можете «отключить» фикстуры локально (т.е., очистить таблицы в БД), используя тег :with_clean_fixture:
context "global state", :with_clean_fixture do
# либо подключив контекст явно
# include_context "any_fixture:clean"
specify "table is empty or smth like this" do
# ...
end
endКак это работает? Группа тестов оборачивается в транзакцию (с помощью before_all), а затем вызывается TestProf::AnyFixture.clean.
Зачистка — это довольно тяжёлая операция. Поэтому рекомендуется избегать подобных ситуаций, когда это возможно.
Статистика использования
AnyFixture позволяет выводит информацию о том, как часто фикстуры были использованы и сколько времени было сэкономлено:
[TEST PROF INFO] AnyFixture usage stats:
key build time hit count saved time
user 00:00.004 4 00:00.017
post 00:00.002 1 00:00.002
Total time spent: 00:00.006
Total time saved: 00:00.019
Total time wasted: 00:00.000Вывод статистики выключен по умолчанию. Вы можете включить его в настройках (TestProf::AnyFixture.config.reporting_enabled = true) или указав переменную окружения ANYFIXTURE_REPORT=1.
Использование автоматических SQL дампов
@since v1.0, experimental
AnyFixture подразумевает генерацию данных для тестов единожды для каждого запуска. Однако даже это может занимать много времени (например, для системных тестов или тестов производительности, где, как правило, требуется много данных); значит, нам необходимо придумать, как ещё лучше оптимизировать популяцию тестовой базы данных!
Представляем вашему вниманию ещё один метод AnyFixture—#register_dump. При первом запуске он работает аналогично #register: выполняет блок кода и отслеживает SQL запросы. Помимо этого AnyFixture также генерирует текстовый SQL дамп из этих запросов, и этот дамп будет использован для последующих запусков тестов — блок кода не будет выполнятся.
Рассмотрим пример:
RSpec.shared_context "account", account: true do
# Как и в случае .register, необходимо использовать .register_dump вне транзакций
before(:all) do
# Блок кода выполняется максимум один раз за весь запуск тестов.
# Код не выполняется вообще, если уже существует соответствующий SQL дамп.
TestProf::AnyFixture.register_dump("account") do
account = FactoryGirl.create(:account, name: "test")
account = Fabricate(:account, name: "test")
account = Account.create!(name: "test")
account.update!(tag: "sql-dump")
end
end
# Так как данные могут быть загружены из SQL дампа, минуя Ruby код,
# мы должны использовать специфичный код для доступа к объектам
let(:account) { Account.find_by!(name: "test") }
endКогда мы запускаем тесты, происходит следующее:
# первый запуск
$ bundle exec rspec
# Вызов AnyFixture.register_dump:
# - Есть ли подходящий SQL дамп? Нет
# - Выполняем блок кода, записываем все модифицирующие запросы в текстовый SQL дамп
# Вызов AnyFixture.clean в конце запуска:
# - удаляем данные из всех затронутых таблиц
# повторной запуск
$ bundle exec rspec
# Вызов AnyFixture.register_dump:
# - Есть ли подходящий SQL дамп? Да
# - Восстанавливаем дамп
# Вызов AnyFixture.clean в конце запуска:
# - удаляем данные из всех затронутых таблицТребования
В настоящий момент поддерживаются только PostgreSQL 12+ и SQLite3.
Инвалидация дампов
Сгенерированный дамп может стать неактуальным по множеству причин: схема БД изменилась, код фикстуры обновился и т.д.. Для отслеживания актуальности дампа AnyFixture использует хэш-суммы соответствующих файлов. По умолчанию это db/schema.rb, db/structure.sql и файл, в котором происходит вызов #register_dump.
Вы можете изменить список файлов, за которыми нужно следить с помощью параметра default_dump_watch_paths:
TestProf::AnyFixture.configure do |config|
# Можно использовать как точные пути, так и маски
config.default_dump_watch_paths << Rails.root.join("spec/factories/**/*")
endТакже вы можете указать дополнительные файлы при вызове #register_dump:
TestProf::AnyFixture.register_dump("account", watch: ["app/models/account.rb", "app/models/account/**/*,rb"]) do
# ...
endПримечание: Когда вы используете опцию watch, текущий файл не добавляется автоматически в список отслеживаемых. Вы можете добавить его вручную, используя __FILE__.
Наконец, если вы хотите умышленно пересоздать дамп, используйте переменную окружения ANYFIXTURE_FORCE_DUMP:
ANYFIXTURE_FORCE_DUMP=1пересоздаст все дампы.ANYFIXTURE_FORCE_DUMP=accountпересоздаст только дампы, принимаемые регулярным выражением/account/.
Дополнительные ключи кэша
Вы также можете указывать ключи для кэширования (в дополнение к хэш-суммам файлов):
# cache_key может быть чем угодно, отвечающим на #to_s
TestProf::AnyFixture.register_dump("account", cache_key: ["str", 1, {key: :val}]) do
# ...
endХуки
before / after
Before хуки вызываются перед блоком кода (для первого запуска) или восстановлением дампа (для повторных запусков). Например, вы можете их использовать для пересоздания схемы в БД:
TestProf::AnyFixture.register_dump(
"account",
before: proc do
begin
Apartment::Tenant.create("test")
rescue
nil
end
Apartment::Tenant.create("test")
end
) do
# ...
endАналогичным образом, after хуки вызываются после выполнения блока кода или восстановления из дампа.
Вы также можете указать глобальные хуки (т.е., вызываемые для всех фикстур):
TestProf::AnyFixture.configure do |config|
config.before_dump do |dump:, import:|
# dump — это объект, содержащий информацию о текущем дампе (например, dump.digest)
# import — это флаг, который равен true тогда и только тогда, когда мы восстанавливаем данные из дампа
end
config.after_dump do |dump:, import:|
# ...
end
endПримечание: after выполняются всегда, даже если блок кода или восстановление дампа упали с ошибкой. Статус создания/восстановления дампа может быть получен с помощью метода dump.success?.
skip_if
Данный хук позволяет полностью пропустить инициализацию фикстуры (как из кода, так и из дампа). Это полезно, если вы хотите сохранить состояние базы данных между запусками тестов (т.е., не выполняете очистку).
Пример:
TestProf::AnyFixture.register_dump(
"account",
# затронутые таблицы не будут добавлены в лист зачистки для AnyFixture.clean (однако другие фикстуры могут их добавить)
clean: false,
skip_if: proc do |dump:|
Apartment::Tenant.switch!("test")
# проверяем, актуальное ли состояние данных в базе
Account.find_by!(name: "test").meta["dump-version"] == dump.digest
end,
before: proc do
begin
Apartment::Tenant.create("test")
rescue
nil
end
Apartment::Tenant.create("test")
end,
after: proc do |dump:, import:|
next if import || !dump.success?
Account.find_by!(name: "test").then do |account|
account.meta["dump-version"] = dump.digest
account.save!
end
end
) do
# ...
endНастройка
Доступны также следующие настройки:
TestProf::AnyFixture.configure do |config|
# Куда сохранять дампы (относительно TestProf.artifact_path)
config.dumps_dir = "any_dumps"
# Включать в дамп подходящие под регулярное выражение запросы (в дополнение к INSERT/UPDATE/DELETE)
config.dump_matching_queries = /^$/
# Использовать или нет консольные утилиты для восстановления дампов (psql или sqlite3)
config.import_dump_via_cli = false
endПримечание: При использовании консольных утилит для восстановления дампов невозможно отследить выполняемые запросы и затронутые таблицы; следовательно, AnyFixture.clean не будет работать.