класс 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
Если вы предварительно загрузите тестовую базу данными со всеми данными фикстур (вероятно, запустив `rails db:fixtures:load`) и используете транзакционные тесты, то можете опустить все объявления фикстур в ваших тестовых случаях, так как все данные уже там, и каждый случай откатывает свои изменения.
Для использования экземплированных фикстур с предварительно загруженными данными установите self.pre_loaded_fixtures в значение true. Это обеспечит доступ к данным фикстур для каждой таблицы, которая была загружена через фикстуры (в зависимости от значения use_instantiated_fixtures).
Когда не следует использовать транзакционные тесты:
-
Вы тестируете, правильно ли работает транзакция. Вложенные транзакции не коммитируются до тех пор, пока не коммитируются все родительские транзакции, в частности, транзакция фикстур, которая начинается в 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 любой фикстуры по её метке, вы можете указать внешние ключи по меткам вместо 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 для внешнего ключа (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 515 def cache_fixtures(connection, fixtures_map) cache_for_connection(connection).update(fixtures_map) end
# File activerecord/lib/active_record/fixtures.rb, line 499 def cache_for_connection(connection) @@all_cached_fixtures[connection] end
# File activerecord/lib/active_record/fixtures.rb, line 507
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 568 def context_class @context_class ||= Class.new end
Базовый класс для контекстов вычислений, используемых фикстурами ERB.
# File activerecord/lib/active_record/fixtures.rb, line 534
def create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base, &block)
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? ? 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(
fixtures_directory,
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 503 def fixture_is_cached?(connection, table_name) cache_for_connection(connection)[table_name] end
# File activerecord/lib/active_record/fixtures.rb, line 559
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. Идентификаторы типа Integer — это значения меньше 2^30. UUID — это RFC 4122 версия 5 SHA-1 хэшей.
# File activerecord/lib/active_record/fixtures.rb, line 528
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 519
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 626 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 495 def reset_cache @@all_cached_fixtures.clear end
Методы публичного экземпляра
# File activerecord/lib/active_record/fixtures.rb, line 638 def [](x) fixtures[x] end
# File activerecord/lib/active_record/fixtures.rb, line 642 def []=(k, v) fixtures[k] = v end
# File activerecord/lib/active_record/fixtures.rb, line 646 def each(&block) fixtures.each(&block) end
# File activerecord/lib/active_record/fixtures.rb, line 650 def size fixtures.size end
# File activerecord/lib/active_record/fixtures.rb, line 656
def table_rows
# allow a standard key to be used for doing defaults in YAML
fixtures.delete("DEFAULTS")
TableRows.new(
table_name,
model_class: model_class,
fixtures: fixtures,
config: config,
).to_hash
end Возвращает словарь строк для вставки. Ключ — таблица, значение — список строк для вставки в эту таблицу.
© 2004–2019 David Heinemeier Hansson
Licensed under the MIT License.