класс ActiveRecord::FixtureSet
Фикстуры — это способ организации данных, с которыми вы хотите протестировать; короче говоря, образцовые данные.
Они хранятся в файлах YAML, по одному файлу на модель, которые размещаются в директории, указанной в ActiveSupport::TestCase.fixture_path=(path) (это автоматически настраивается для Rails, поэтому вы можете просто поместить свои файлы в <your-rails-app>/test/fixtures/). Файл фикстуры заканчивается расширением .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.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" do assert_equal "Ruby on Rails", 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 с вашими фикстурами YAML, чтобы создать множество фикстур для тестирования на нагрузку, например:
<% 1.upto(1000) do |i| %> fix_<%= i %>: id: <%= i %> name: guy_<%= 1 %> <% end %>
Это создаст 1000 очень простых фикстур.
Используя ERB, вы также можете вводить динамические значения в свои фикстуры с вставками, такими как <%=
Date.today.strftime("%Y-%m-%d") %>. Однако это функция, которую следует использовать с осторожностью. Суть фикстур заключается в том, что они представляют собой стабильные единицы предсказуемых образцовых данных. Если вы чувствуете необходимость вводить динамические значения, возможно, вам следует пересмотреть, правильно ли ваша программа тестируется. Поэтому динамические значения в фикстурах следует рассматривать как признак проблемного кода.
Вспомогательные методы, определенные в фикстуре, не будут доступны в других фикстурах, чтобы предотвратить нежелательные межтестовые зависимости. Методы, используемые несколькими фикстурами, должны быть определены в модуле, который включается в ActiveRecord::FixtureSet.context_class.
-
определить вспомогательный метод в `test_helper.rb`
module FixtureFileHelpers def file_sha(path) Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path))) end end ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers -
использовать вспомогательный метод в фикстуре
photo: name: kitten.png sha: <%= file_sha 'files/kitten.png' %>
Транзакционные фикстуры
Тестовые случаи могут использовать begin+rollback для изоляции своих изменений в базе данных вместо необходимости удаления+вставки для каждого тестового случая.
class FooTest < ActiveSupport::TestCase
self.use_transactional_fixtures = true
test "godzilla" do
assert !Foo.all.empty?
Foo.destroy_all
assert Foo.all.empty?
end
test "godzilla aftermath" do
assert !Foo.all.empty?
end
end
Если вы предварительно загрузили свою тестовую базу данных всеми данными фикстур (вероятно, в задаче rake) и используете транзакционные фикстуры, то вы можете опустить все объявления фикстур в своих тестовых случаях, так как все данные уже там, и каждый случай откатывает свои изменения.
Для использования созданных экземпляров фикстур с предварительно загруженными данными установите 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
Время, чтобы дать нашей обезьяне фрукты.
### 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) %>
Поддержка значений по умолчанию YAML
Вы можете устанавливать и повторно использовать значения по умолчанию в файлах фикстур YAML. Этот же метод используется в файле database.yml для указания значений по умолчанию:
DEFAULTS: &DEFAULTS created_on: <%= 3.weeks.ago.to_s(:db) %> first: name: Smurf <<: *DEFAULTS second: name: Fraggle <<: *DEFAULTS
Любая фикстура с меткой «DEFAULTS» безопасно игнорируется.
Константы
- MAX_ID
Атрибуты
Методы публичного класса
# File activerecord/lib/active_record/fixtures.rb, line 439 def self.cache_fixtures(connection, fixtures_map) cache_for_connection(connection).update(fixtures_map) end
# File activerecord/lib/active_record/fixtures.rb, line 423 def self.cache_for_connection(connection) @@all_cached_fixtures[connection] end
# File activerecord/lib/active_record/fixtures.rb, line 431
def self.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 562 def self.context_class @context_class ||= Class.new end
Базовый класс для контекстов вычислений, используемых фикстурами ERB.
# File activerecord/lib/active_record/fixtures.rb, line 496
def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
fixture_set_names = Array(fixture_set_names).map(&:to_s)
class_names = ClassCache.new class_names, config
# FIXME: Apparently JK uses this.
connection = block_given? ? yield : ActiveRecord::Base.connection
files_to_read = fixture_set_names.reject { |fs_name|
fixture_is_cached?(connection, fs_name)
}
unless files_to_read.empty?
connection.disable_referential_integrity do
fixtures_map = {}
fixture_sets = files_to_read.map do |fs_name|
klass = class_names[fs_name]
conn = klass ? klass.connection : connection
fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
conn,
fs_name,
klass,
::File.join(fixtures_directory, fs_name))
end
update_all_loaded_fixtures fixtures_map
connection.transaction(:requires_new => true) do
fixture_sets.each do |fs|
conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
table_rows = fs.table_rows
table_rows.each_key do |table|
conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
end
table_rows.each do |fixture_set_name, rows|
rows.each do |row|
conn.insert_fixture(row, fixture_set_name)
end
end
# Cap primary key sequences to max(pk).
if conn.respond_to?(:reset_pk_sequence!)
conn.reset_pk_sequence!(fs.table_name)
end
end
end
cache_fixtures(connection, fixtures_map)
end
end
cached_fixtures(connection, fixture_set_names)
end # File activerecord/lib/active_record/fixtures.rb, line 427 def self.fixture_is_cached?(connection, table_name) cache_for_connection(connection)[table_name] end
# File activerecord/lib/active_record/fixtures.rb, line 553
def self.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. Идентификаторы типа Integer имеют значения меньше 2^30. UUID — это хэши RFC 4122 версии 5 SHA-1.
# File activerecord/lib/active_record/fixtures.rb, line 455
def self.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 443
def self.instantiate_fixtures(object, fixture_set, load_instances = true)
if load_instances
fixture_set.each do |fixture_name, fixture|
begin
object.instance_variable_set "@#{fixture_name}", fixture.find
rescue FixtureClassNotFound
nil
end
end
end
end # File activerecord/lib/active_record/fixtures.rb, line 572
def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
@name = name
@path = path
@config = config
@model_class = nil
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
@model_class = class_name
else
@model_class = class_name.safe_constantize if class_name
end
@connection = connection
@table_name = ( model_class.respond_to?(:table_name) ?
model_class.table_name :
self.class.default_fixture_table_name(name, config) )
@fixtures = read_fixture_files path, @model_class
end # File activerecord/lib/active_record/fixtures.rb, line 419 def self.reset_cache @@all_cached_fixtures.clear end
Методы публичного экземпляра
# File activerecord/lib/active_record/fixtures.rb, line 593 def [](x) fixtures[x] end
# File activerecord/lib/active_record/fixtures.rb, line 597 def []=(k,v) fixtures[k] = v end
# File activerecord/lib/active_record/fixtures.rb, line 601 def each(&block) fixtures.each(&block) end
# File activerecord/lib/active_record/fixtures.rb, line 605 def size fixtures.size end
# File activerecord/lib/active_record/fixtures.rb, line 611
def table_rows
now = config.default_timezone == :utc ? Time.now.utc : Time.now
now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
fixtures.delete('DEFAULTS')
# track any join tables we need to insert later
rows = Hash.new { |h,table| h[table] = [] }
rows[table_name] = fixtures.map do |label, fixture|
row = fixture.to_hash
if model_class
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
timestamp_column_names.each do |c_name|
row[c_name] = now unless row.key?(c_name)
end
end
# interpolate the fixture label
row.each do |key, value|
row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String)
end
# generate a primary key if necessary
if has_primary_key_column? && !row.include?(primary_key_name)
row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type)
end
# If STI is used, find the correct subclass for association reflection
reflection_class =
if row.include?(inheritance_column_name)
row[inheritance_column_name].constantize rescue model_class
else
model_class
end
reflection_class._reflections.each_value do |association|
case association.macro
when :belongs_to
# Do not replace association name with association foreign key if they are named the same
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
# support polymorphic belongs_to as "label (Type)"
row[association.foreign_type] = $1
end
fk_type = reflection_class.columns_hash[fk_name].type
row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
end
when :has_many
if association.options[:through]
add_join_records(rows, row, HasManyThroughProxy.new(association))
end
end
end
end
row
end
rows
end Возвращает хэш строк для вставки. Ключ — таблица, значение — список строк для вставки в эту таблицу.
© 2004–2018 David Heinemeier Hansson
Licensed under the MIT License.