класс 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 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 с вашими фикстурами YAML, чтобы создать множество фикстур для нагрузочного тестирования, например:
<% 1.upto(1000) do |i| %> fix_<%= i %>: id: <%= i %> name: guy_<%= i %> <% end %>
Это создаст 1000 очень простых фикстур.
Используя ERB, вы также можете вводить динамические значения в свои фикстуры с вставками, такими как <%=
Date.today.strftime("%Y-%m-%d") %>. Однако это функция, которую следует использовать с осторожностью. Цель фикстур — это стабильные единицы предсказуемых образцовых данных. Если вы чувствуете, что вам нужно вводить динамические значения, возможно, вам следует пересмотреть, правильно ли ваша программа тестируется. Следовательно, динамические значения в фикстурах следует рассматривать как признак плохой архитектуры.
Вспомогательные методы, определённые в фикстуре, не будут доступны в других фикстурах, чтобы предотвратить нежелательные взаимозависимости между тестами. Методы, используемые несколькими фикстурами, должны быть определены в модуле, включённом в ::context_class.
-
определить вспомогательный метод в
test_helper.rbmodule FixtureFileHelpers def file_sha(path) Digest::SHA2.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
Если вы предварительно загружаете тестовую базу данных со всеми данными фикстур (вероятно, в задаче rake) и используете транзакционные тесты, вы можете опустить все объявления фикстур в своих тестовых случаях, поскольку все данные уже там, и каждый случай откатывает свои изменения.
Для использования созданных фикстур с предварительно загруженными данными установите self.pre_loaded_fixtures в true. Это обеспечит доступ к данным фикстур для каждой таблицы, которая была загружена через фикстуры (в зависимости от значения use_instantiated_fixtures).
Когда не следует использовать транзакционные тесты:
-
Вы проверяете, работает ли транзакция правильно. Вложенные транзакции не выполняют commit, пока не выполнят commit все родительские транзакции, в частности, транзакция фикстур, которая начинается в setup и откатывается в teardown. Таким образом, вы не сможете проверить результаты вашей транзакции, пока Active Record не будет поддерживать вложенные транзакции или точки сохранения (в процессе).
-
Ваша база данных не поддерживает транзакции. Каждая база данных Active Record поддерживает транзакции, за исключением MySQL MyISAM. Используйте InnoDB, MaxDB или NDB вместо неё.
Расширенные фикстуры
Фикстуры, которые не указывают ID, получают дополнительные функции:
-
Стабильные, автоматически сгенерированные ID
-
Ссылки на метки для ассоциаций (belongs_to, has_one, has_many)
-
Ассоциации HABTM в виде списков в строке
Существуют некоторые расширенные функции, доступные даже если указан ID:
-
Автозаполнение столбцов timestamp
-
Интерполяция меток фикстур
-
Поддержка значений по умолчанию YAML
Стабильные, автогенерированные ID
Вот фикстура обезьяны:
george: id: 1 name: George the Monkey reginald: id: 2 name: Reginald the Pirate
У каждой из этих фикстур есть два уникальных идентификатора: один для базы данных и один для человека. Почему бы нам не сгенерировать первичный ключ вместо этого? Хеширование каждой метки фикстуры даёт постоянный ID:
george: # generated id: 503576764 name: George the Monkey reginald: # generated id: 324201669 name: Reginald the Pirate
Active Record обращается к классу модели фикстуры, определяет правильный первичный ключ и генерирует его непосредственно перед вставкой фикстуры в базу данных.
Сгенерированный ID для заданной метки является постоянным, поэтому мы можем определить ID любой фикстуры без загрузки чего-либо, если знаем метку.
Ссылки на метки для ассоциаций (belongs_to, has_one, has_many)
Указание внешних ключей в фикстурах может быть очень хрупким, не говоря уже о сложности чтения. Поскольку Active Record может определить ID любой фикстуры по её метке, вы можете указать FK по метке вместо ID.
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
Добавим ещё несколько обезьян и пиратов и разделим это на несколько файлов, и становится довольно сложно отслеживать, что происходит. Давайте используем метки вместо ID:
### in pirates.yml reginald: name: Reginald the Pirate monkey: george ### in monkeys.yml george: name: George the Monkey pirate: reginald
Всё стало ясно. Active Record анализирует класс модели фикстуры, находит все belongs_to ассоциации и позволяет вам указать целевую **метку** для **ассоциации** (обезьяна: george) вместо целевого **ID** для **FK** (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
Давайте уберем фикстуру 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 ассоциации.
Автозаполнение столбцов Timestamp
Если ваша таблица/модель указывает любые стандартные столбцы timestamp 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» безопасно игнорируется.
Настройка класса модели фикстуры
Можно установить класс модели фикстуры напрямую в файле YAML. Это полезно, когда фикстуры загружаются за пределами тестов, и set_fixture_class недоступен (например, при запуске rails db:fixtures:load).
_fixture: model_class: User david: name: David
Любые фикстуры с меткой «_fixture» безопасно игнорируются.
Константы
- MAX_ID
Атрибуты
Методы публичного класса
# File activerecord/lib/active_record/fixtures.rb, line 475 def self.cache_fixtures(connection, fixtures_map) cache_for_connection(connection).update(fixtures_map) end
# File activerecord/lib/active_record/fixtures.rb, line 459 def self.cache_for_connection(connection) @@all_cached_fixtures[connection] end
# File activerecord/lib/active_record/fixtures.rb, line 467
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 590 def self.context_class @context_class ||= Class.new end
Базовый класс для контекстов вычисления, используемых в фикстурах ERB.
# File activerecord/lib/active_record/fixtures.rb, line 531
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?
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
fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection }
fixture_sets_by_connection.each do |conn, set|
table_rows_for_connection = Hash.new { |h, k| h[k] = [] }
set.each do |fs|
fs.table_rows.each do |table, rows|
table_rows_for_connection[table].unshift(*rows)
end
end
conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys)
# Cap primary key sequences to max(pk).
if conn.respond_to?(:reset_pk_sequence!)
set.each { |fs| conn.reset_pk_sequence!(fs.table_name) }
end
end
cache_fixtures(connection, fixtures_map)
end
cached_fixtures(connection, fixture_set_names)
end # File activerecord/lib/active_record/fixtures.rb, line 463 def self.fixture_is_cached?(connection, table_name) cache_for_connection(connection)[table_name] end
# File activerecord/lib/active_record/fixtures.rb, line 581
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 491
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 479
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 600
def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
@name = name
@path = path
@config = config
self.model_class = class_name
@fixtures = read_fixture_files(path)
@connection = connection
@table_name = (model_class.respond_to?(:table_name) ?
model_class.table_name :
self.class.default_fixture_table_name(name, config))
end # File activerecord/lib/active_record/fixtures.rb, line 455 def self.reset_cache @@all_cached_fixtures.clear end
Методы публичного экземпляра
# File activerecord/lib/active_record/fixtures.rb, line 616 def [](x) fixtures[x] end
# File activerecord/lib/active_record/fixtures.rb, line 620 def []=(k, v) fixtures[k] = v end
# File activerecord/lib/active_record/fixtures.rb, line 624 def each(&block) fixtures.each(&block) end
# File activerecord/lib/active_record/fixtures.rb, line 628 def size fixtures.size end
# File activerecord/lib/active_record/fixtures.rb, line 634
def table_rows
now = config.default_timezone == :utc ? Time.now.utc : Time.now
# 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
# Resolve enums
model_class.defined_enums.each do |name, values|
if row.include?(name)
row[name] = values.fetch(row[name], row[name])
end
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.type_for_attribute(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.