класс 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 не упорядочены.
Если вам нужны упорядоченные фикстуры, используйте тип omap YAML. См. 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
В качестве альтернативы вы можете включить автоматическую инициализацию данных фикстуры. Например, рассмотрите следующие тесты:
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") %>. Однако это функция, которую следует использовать с осторожностью. Суть фикстур заключается в том, что они являются стабильными единицами предсказуемых образцовых данных. Если вы чувствуете, что вам нужно вводить динамические значения, то, возможно, вам следует пересмотреть, может ли ваше приложение быть должным образом протестировано. Следовательно, динамические значения в фикстурах следует рассматривать как признак плохой архитектуры кода.
Вспомогательные методы, определённые в фикстуре, недоступны в других фикстурах, чтобы предотвратить нежелательные межтестовые зависимости. Методы, используемые несколькими фикстурами, должны быть определены в модуле, который включается в 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, а не целевой идентификатор для 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
Давайте уберём фикстуру fruits_monkeys.yml.
### 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, query_constraints: [:shop_id, :order_id]
belongs_to :book, query_constraints: [: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 556 def cache_fixtures(connection, fixtures_map) cache_for_connection(connection).update(fixtures_map) end
# File activerecord/lib/active_record/fixtures.rb, line 540 def cache_for_connection(connection) @@all_cached_fixtures[connection] end
# File activerecord/lib/active_record/fixtures.rb, line 548
def cached_fixtures(connection, keys_to_fetch = nil)
if keys_to_fetch
cache_for_connection(connection).values_at(*keys_to_fetch)
else
cache_for_connection(connection).values
end
end # File activerecord/lib/active_record/fixtures.rb, line 615
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 623 def context_class @context_class ||= Class.new end
Базовый класс для контекстов вычисления, используемых фикстурами ERB.
# File activerecord/lib/active_record/fixtures.rb, line 575
def create_fixtures(fixtures_directories, fixture_set_names, class_names = {}, config = ActiveRecord::Base, &block)
fixture_set_names = Array(fixture_set_names).map(&:to_s)
class_names.stringify_keys!
# FIXME: Apparently JK uses this.
connection = block_given? ? block : lambda { ActiveRecord::Base.connection }
fixture_files_to_read = fixture_set_names.reject do |fs_name|
fixture_is_cached?(connection.call, fs_name)
end
if fixture_files_to_read.any?
fixtures_map = read_and_insert(
Array(fixtures_directories),
fixture_files_to_read,
class_names,
connection,
)
cache_fixtures(connection.call, fixtures_map)
end
cached_fixtures(connection.call, fixture_set_names)
end # File activerecord/lib/active_record/fixtures.rb, line 544 def fixture_is_cached?(connection, table_name) cache_for_connection(connection)[table_name] end
# File activerecord/lib/active_record/fixtures.rb, line 601
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 569
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 560
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 693 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 536 def reset_cache @@all_cached_fixtures.clear end
Публичные методы экземпляра
# File activerecord/lib/active_record/fixtures.rb, line 704 def [](x) fixtures[x] end
# File activerecord/lib/active_record/fixtures.rb, line 708 def []=(k, v) fixtures[k] = v end
# File activerecord/lib/active_record/fixtures.rb, line 712 def each(&block) fixtures.each(&block) end
# File activerecord/lib/active_record/fixtures.rb, line 716 def size fixtures.size end
# File activerecord/lib/active_record/fixtures.rb, line 722
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.