класс ActiveRecord::FixtureSet
Наборы фикстур Active Record
Фикстуры — это способ организации данных, с которыми вы хотите работать при тестировании; короче говоря, образцы данных.
Они хранятся в файлах YAML, по одному файлу на модель, которые по умолчанию размещаются в папке <your-rails-app>/test/fixtures/ или в папке test/fixtures под любым из ваших движков приложения.
Расположение также можно изменить с помощью ActiveSupport::TestCase.fixture_paths=, после того, как вы require "rails/test_help" в своей папке test_helper.rb.
Файл фикстуры заканчивается расширением файла .yml, например: <your-rails-app>/test/fixtures/web_sites.yml).
Формат файла фикстуры выглядит так:
rubyonrails: id: 1 name: Ruby on Rails url: http://www.rubyonrails.org google: id: 2 name: Google url: http://www.google.com
Этот файл фикстур содержит две фикстуры. Каждая фикстура YAML (т.е. запись) получает имя и после неё следует отстуженный список пар ключ/значение в формате «ключ: значение». Записи разделены пустой строкой для вашего удобства просмотра.
Порядок
Фикстуры по умолчанию не упорядочены. Это связано с тем, что отображения в YAML не упорядочены.
Если вам нужны упорядоченные фикстуры, используйте тип YAML omap. См. yaml.org/type/omap.html для спецификации.
Вам понадобятся упорядоченные фикстуры, когда у вас есть ограничения внешнего ключа на ключах в одной и той же таблице. Это часто требуется для древовидных структур.
Например:
--- !omap
- parent:
id: 1
parent_id: NULL
title: Parent
- child:
id: 2
parent_id: 1
title: Child Использование фикстур в тестовых случаях
Поскольку фикстуры являются инструментом для тестирования, мы используем их в наших модульных и функциональных тестах. Существует два способа использования фикстур, но сначала давайте посмотрим на пример модульного теста:
require "test_helper"
class WebSiteTest < ActiveSupport::TestCase
test "web_site_count" do
assert_equal 2, WebSite.count
end
end
По умолчанию test_helper.rb загрузит все ваши фикстуры в вашу тестовую базу данных, поэтому этот тест будет успешным.
Тестовая среда автоматически загрузит все фикстуры в базу данных перед каждым тестом. Для обеспечения согласованности данных среда удаляет фикстуры перед выполнением загрузки.
Помимо наличия в базе данных, данные фикстуры также могут быть доступны с помощью специального динамического метода, имя которого совпадает с именем модели.
Передача имени фикстуры в этот динамический метод возвращает фикстуру, соответствующую этому имени:
test "find one" do assert_equal "Ruby on Rails", web_sites(:rubyonrails).name end
Передача нескольких имён фикстур возвращает все фикстуры, соответствующие этим именам:
test "find all by name" do assert_equal 2, web_sites(:rubyonrails, :google).length end
Передача без аргументов возвращает все фикстуры:
test "find all" do assert_equal 2, web_sites.length end
Передача любого имени фикстуры, которой не существует, вызовет StandardError:
test "find by name that does not exist" do
assert_raise(StandardError) { web_sites(:reddit) }
end
Если имена моделей конфликтуют с TestCase методами, вы можете использовать универсальный fixture аксессор
test "generic find" do assert_equal "Ruby on Rails", fixture(:web_sites, :rubyonrails).name end
В качестве альтернативы вы можете включить автоматическое создание экземпляров данных фикстур. Например, рассмотрим следующие тесты:
test "find_alt_method_1" do assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name'] end test "find_alt_method_2" do assert_equal "Ruby on Rails", @rubyonrails.name end
Чтобы использовать эти методы для доступа к данным фикстур в ваших тестовых случаях, вы должны указать одно из следующих в вашем классе, производном от ActiveSupport::TestCase:
-
полностью включить создание экземпляров фикстур (включить альтернативные методы #1 и #2 выше)
self.use_instantiated_fixtures = true
-
создать только хеш для фикстур, не «находя» каждый экземпляр (включить только альтернативный метод #1)
self.use_instantiated_fixtures = :no_instances
Использование любого из этих альтернативных методов влечёт за собой потерю производительности, так как для создания хеша фикстуры и/или переменных экземпляра необходимо полностью пройтись по данным фикстур в базе данных. Это дорого для больших наборов данных фикстур.
Динамические фикстуры с ERB
Иногда вам не так важны данные фикстур, как объём. В таких случаях вы можете смешивать ERB со своими фикстурами YAML, чтобы создать кучу фикстур для нагрузочного тестирования, например:
<% 1.upto(1000) do |i| %> fix_<%= i %>: id: <%= i %> name: guy_<%= i %> <% end %>
Это создаст 1000 очень простых фикстур.
Используя ERB, вы также можете внедрять динамические значения в свои фикстуры с вставками, подобными <%= Date.today.strftime("%Y-%m-%d") %>. Однако эта функция должна использоваться с осторожностью. Цель фикстур — быть стабильными единицами предсказуемых образцов данных. Если вам нужно внедрять динамические значения, возможно, вам следует пересмотреть, правильно ли ваш application тестуем. Поэтому динамические значения в фикстурах следует рассматривать как признак плохой архитектуры.
Вспомогательные методы, определённые в фикстуре, не будут доступны в других фикстурах, чтобы предотвратить нежелательные межтестовые зависимости. Методы, используемые несколькими фикстурами, должны быть определены в модуле, который включён в ActiveRecord::FixtureSet.context_class.
-
определите вспомогательный метод в
test_helper.rbmodule FixtureFileHelpers def file_sha(path) OpenSSL::Digest::SHA256.hexdigest(File.read(Rails.root.join('test/fixtures', path))) end end ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers -
используйте вспомогательный метод в фикстуре
photo: name: kitten.png sha: <%= file_sha 'files/kitten.png' %>
Транзакционные тесты
Тестовые случаи могут использовать begin+rollback для изоляции своих изменений в базе данных вместо того, чтобы удалять+вставлять для каждого тестового случая.
class FooTest < ActiveSupport::TestCase
self.use_transactional_tests = true
test "godzilla" do
assert_not_empty Foo.all
Foo.destroy_all
assert_empty Foo.all
end
test "godzilla aftermath" do
assert_not_empty Foo.all
end
end
Если вы предварительно загрузите свою тестовую базу данных всеми данными фикстур (вероятно, запустив bin/rails db:fixtures:load) и используете транзакционные тесты, то вы можете опустить все объявления фикстур в ваших тестовых случаях, так как все данные уже там и каждый случай откатывает свои изменения.
Для использования экземпляризированных фикстур с предварительно загруженными данными установите self.pre_loaded_fixtures в true. Это обеспечит доступ к данным фикстур для каждой таблицы, которая была загружена через фикстуры (в зависимости от значения use_instantiated_fixtures).
Когда не следует использовать транзакционные тесты:
-
Вы тестируете, работает ли транзакция правильно. Вложенные транзакции не выполняются до тех пор, пока все родительские транзакции не выполнятся, в частности, транзакция фикстур, которая начинается в настройках и откатывается в завершении. Таким образом, вы не сможете проверить результаты своей транзакции до тех пор, пока Active Record не будет поддерживать вложенные транзакции или точки сохранения (на стадии разработки).
-
Ваша база данных не поддерживает транзакции. Каждая база данных Active Record поддерживает транзакции, за исключением MySQL MyISAM. Используйте InnoDB, MaxDB или NDB вместо неё.
Расширенные фикстуры
Фикстуры, которые не указывают идентификатор, получают дополнительные функции:
-
Стабильные, автоматически сгенерированные идентификаторы
-
Ссылки на метки для ассоциаций (belongs_to, has_one, has_many)
-
Ассоциации HABTM как встроенные списки
Доступны некоторые расширенные функции, даже если идентификатор указан:
-
Автозаполнение столбцов со временем
-
Интерполяция меток фикстур
-
Поддержка значений по умолчанию YAML
Стабильные, автоматически сгенерированные идентификаторы
Вот фикстура обезьяны:
george: id: 1 name: George the Monkey reginald: id: 2 name: Reginald the Pirate
Каждая из этих фикстур имеет два уникальных идентификатора: один для базы данных и один для людей. Почему бы не сгенерировать первичный ключ вместо этого? Хэширование метки каждой фикстуры даёт постоянный идентификатор:
george: # generated id: 503576764 name: George the Monkey reginald: # generated id: 324201669 name: Reginald the Pirate
Active Record рассматривает класс модели фикстуры, обнаруживает правильный первичный ключ и генерирует его непосредственно перед вставкой фикстуры в базу данных.
Сгенерированный идентификатор для данной метки является постоянным, поэтому мы можем определить идентификатор любой фикстуры без загрузки чего-либо, если знаем её метку.
Ссылки на метки для ассоциаций (belongs_to, has_one, has_many
Указание внешних ключей в фикстурах может быть очень ненадёжным, не говоря уже о сложности чтения. Так как Active Record может определить идентификатор любой фикстуры по её метке, вы можете указать внешние ключи по метке вместо идентификатора.
belongs_to
Давайте рассмотрим других обезьян и пиратов.
### in pirates.yml reginald: id: 1 name: Reginald the Pirate monkey_id: 1
### in monkeys.yml george: id: 1 name: George the Monkey pirate_id: 1
Добавьте ещё несколько обезьян и пиратов и разбейте это на несколько файлов, и становится довольно трудно отслеживать, что происходит. Давайте используем метки вместо идентификаторов:
### in pirates.yml reginald: name: Reginald the Pirate monkey: george
### in monkeys.yml george: name: George the Monkey pirate: reginald
Всё стало ясно. Active Record отражает класс модели фикстуры, находит все belongs_to ассоциации и позволяет указать целевую метку для ассоциации (обезьяна: джордж) вместо целевого идентификатора для внешнего ключа (monkey_id: 1).
Полиморфный belongs_to
Поддержка полиморфных отношений немного сложнее, так как Active Record должен знать, к какому типу указывает ваша ассоциация. Нечто подобное должно быть вам знакомо:
### in fruit.rb belongs_to :eater, polymorphic: true
### in fruits.yml apple: id: 1 name: apple eater_id: 1 eater_type: Monkey
Можно ли сделать лучше? Конечно!
apple: eater: george (Monkey)
Просто укажите тип полиморфной цели, и Active Record позаботится обо всём остальном.
has_and_belongs_to_many или has_many :through
Пришло время дать нашей обезьяне фруктов.
### in monkeys.yml george: id: 1 name: George the Monkey
### in fruits.yml apple: id: 1 name: apple orange: id: 2 name: orange grape: id: 3 name: grape
### in fruits_monkeys.yml apple_george: fruit_id: 1 monkey_id: 1 orange_george: fruit_id: 2 monkey_id: 1 grape_george: fruit_id: 3 monkey_id: 1
Давайте уберём фикстуру HABTM.
### in monkeys.yml george: id: 1 name: George the Monkey fruits: apple, orange, grape
### in fruits.yml apple: name: apple orange: name: orange grape: name: grape
Всё готово! Больше нет файла fruits_monkeys.yml. Мы указали список фруктов для фикстуры Джорджа, но так же легко можно было бы указать список обезьян для каждого фрукта. Как и в случае с belongs_to, Active Record отражает класс модели фикстуры и находит has_and_belongs_to_many ассоциации.
Автозаполнение столбцов со временем
Если ваша таблица/модель указывает какие-либо стандартные столбцы со временем Active Record (created_at, created_on, updated_at, updated_on), они будут автоматически установлены в значение Time.now.
Если вы установили конкретные значения, они останутся без изменений.
Интерполяция меток фикстур
Метка текущей фикстуры всегда доступна как значение столбца:
geeksomnia: name: Geeksomnia's Account subdomain: $LABEL email: $LABEL@email.com
Кроме того, иногда (например, при переносе старых фикстур с таблицами соединения) вам потребуется получить идентификатор для данной метки. ERB на помощь:
george_reginald: monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %> pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %>
Если модель использует UUID для идентификаторов, добавьте аргумент :uuid:
ActiveRecord::FixtureSet.identify(:boaty_mcboatface, :uuid)
Поддержка значений по умолчанию YAML
Вы можете устанавливать и повторно использовать значения по умолчанию в файле YAML ваших фикстур. Это та же техника, которая используется в файле database.yml для указания значений по умолчанию:
DEFAULTS: &DEFAULTS created_on: <%= 3.weeks.ago.to_fs(:db) %> first: name: Smurf <<: *DEFAULTS second: name: Fraggle <<: *DEFAULTS
Любая фикстура с меткой «DEFAULTS» безопасно игнорируется.
Помимо использования «DEFAULTS», вы также можете указать, какие фикстуры будут игнорироваться, установив «ignore» в секции «_fixture».
# users.yml
_fixture:
ignore:
- base
# or use "ignore: base" when there is only one fixture that needs to be ignored.
base: &base
admin: false
introduction: "This is a default description"
admin:
<<: *base
admin: true
visitor:
<<: *base В приведенном выше примере «base» будет проигнорировано при создании фикстур. Это можно использовать для наследования общих атрибутов.
Фикстуры составного первичного ключа
Фикстуры для таблиц с составным первичным ключом довольно похожи на обычные таблицы. При использовании столбца id, столбец может быть опущен как обычно:
# app/models/book.rb class Book < ApplicationRecord self.primary_key = [:author_id, :id] belongs_to :author end
# books.yml alices_adventure_in_wonderland: author_id: <%= ActiveRecord::FixtureSet.identify(:lewis_carroll) %> title: "Alice's Adventures in Wonderland"
Однако, для поддержки составных первичных ключей, необходимо использовать метод «composite_identify»:
# app/models/book_orders.rb class BookOrder < ApplicationRecord self.primary_key = [:shop_id, :id] belongs_to :order, foreign_key: [:shop_id, :order_id] belongs_to :book, foreign_key: [:author_id, :book_id] end
# book_orders.yml
alices_adventure_in_wonderland_in_books:
author: lewis_carroll
book_id: <%= ActiveRecord::FixtureSet.composite_identify(
:alices_adventure_in_wonderland, Book.primary_key)[:id] %>
shop: book_store
order_id: <%= ActiveRecord::FixtureSet.composite_identify(
:books, Order.primary_key)[:id] %> Настройка класса модели фикстуры
Возможна прямая установка класса модели фикстуры в файле YAML. Это полезно, когда фикстуры загружаются вне тестов и set_fixture_class недоступен (например, при выполнении bin/rails db:fixtures:load).
_fixture: model_class: User david: name: David
Любые фикстуры с меткой «_fixture» будут безопасно проигнорированы.
Константы
- MAX_ID
Атрибуты
Публичные методы класса
# File activerecord/lib/active_record/fixtures.rb, line 578 def cache_fixtures(connection_pool, fixtures_map) cache_for_connection_pool(connection_pool).update(fixtures_map) end
# File activerecord/lib/active_record/fixtures.rb, line 562 def cache_for_connection_pool(connection_pool) @@all_cached_fixtures[connection_pool] end
# File activerecord/lib/active_record/fixtures.rb, line 570
def cached_fixtures(connection_pool, keys_to_fetch = nil)
if keys_to_fetch
cache_for_connection_pool(connection_pool).values_at(*keys_to_fetch)
else
cache_for_connection_pool(connection_pool).values
end
end # File activerecord/lib/active_record/fixtures.rb, line 635
def composite_identify(label, key)
key
.index_with
.with_index { |sub_key, index| (identify(label) << index) % MAX_ID }
.with_indifferent_access
end Возвращает согласованное, независимое от платформы хэширование, представляющее отображение между меткой и подкомпонентами предоставленного составного ключа.
Пример:
composite_identify("label", [:a, :b, :c]) # => { a: hash_1, b: hash_2, c: hash_3 }
# File activerecord/lib/active_record/fixtures.rb, line 643 def context_class @context_class ||= Class.new end
Базовый класс для контекстов оценки, используемых фикстурами ERB.
# File activerecord/lib/active_record/fixtures.rb, line 597
def create_fixtures(fixtures_directories, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
fixture_set_names = Array(fixture_set_names).map(&:to_s)
class_names.stringify_keys!
connection_pool = config.connection_pool
fixture_files_to_read = fixture_set_names.reject do |fs_name|
fixture_is_cached?(connection_pool, fs_name)
end
if fixture_files_to_read.any?
fixtures_map = read_and_insert(
Array(fixtures_directories),
fixture_files_to_read,
class_names,
connection_pool,
)
cache_fixtures(connection_pool, fixtures_map)
end
cached_fixtures(connection_pool, fixture_set_names)
end # File activerecord/lib/active_record/fixtures.rb, line 566 def fixture_is_cached?(connection_pool, table_name) cache_for_connection_pool(connection_pool)[table_name] end
# File activerecord/lib/active_record/fixtures.rb, line 621
def identify(label, column_type = :integer)
if column_type == :uuid
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
else
Zlib.crc32(label.to_s) % MAX_ID
end
end Возвращает согласованный, независимый от платформы идентификатор для label.
Целочисленные идентификаторы – значения меньше 2^30. UUID – это RFC 4122 версии 5 SHA-1 хэши.
# File activerecord/lib/active_record/fixtures.rb, line 591
def instantiate_all_loaded_fixtures(object, load_instances = true)
all_loaded_fixtures.each_value do |fixture_set|
instantiate_fixtures(object, fixture_set, load_instances)
end
end # File activerecord/lib/active_record/fixtures.rb, line 582
def instantiate_fixtures(object, fixture_set, load_instances = true)
return unless load_instances
fixture_set.each do |fixture_name, fixture|
object.instance_variable_set "@#{fixture_name}", fixture.find
rescue FixtureClassNotFound
nil
end
end # File activerecord/lib/active_record/fixtures.rb, line 715 def initialize(_, name, class_name, path, config = ActiveRecord::Base) @name = name @path = path @config = config self.model_class = class_name @fixtures = read_fixture_files(path) @table_name = model_class&.table_name || self.class.default_fixture_table_name(name, config) end
# File activerecord/lib/active_record/fixtures.rb, line 558 def reset_cache @@all_cached_fixtures.clear end
Публичные методы экземпляра
# File activerecord/lib/active_record/fixtures.rb, line 726 def [](x) fixtures[x] end
# File activerecord/lib/active_record/fixtures.rb, line 730 def []=(k, v) fixtures[k] = v end
# File activerecord/lib/active_record/fixtures.rb, line 734 def each(&block) fixtures.each(&block) end
# File activerecord/lib/active_record/fixtures.rb, line 738 def size fixtures.size end
# File activerecord/lib/active_record/fixtures.rb, line 744
def table_rows
# allow specifying fixtures to be ignored by setting `ignore` in `_fixture` section
fixtures.except!(*ignored_fixtures)
TableRows.new(
table_name,
model_class: model_class,
fixtures: fixtures,
).to_hash
end Возвращает хеш строк для вставки. Ключ — таблица, значение — список строк для вставки в эту таблицу.
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.