Spec-Zone.ru › Ruby on Rails 7.1

класс 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.rb

    module 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).

Когда не следует использовать транзакционные тесты:

  1. Вы тестируете, работает ли транзакция правильно. Вложенные транзакции не выполняются до тех пор, пока не завершатся все родительские транзакции, особенно транзакция фикстур, которая начинается в настройке и отменяется при завершении. Таким образом, вы не сможете проверить результаты своей транзакции до тех пор, пока Active Record не будет поддерживать вложенные транзакции или точки сохранения (в разработке).

  2. Ваша база данных не поддерживает транзакции. Каждая база данных 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

Атрибуты

config[R]
fixtures[R]
ignored_fixtures[R]
model_class[R]
name[R]
table_name[R]

Публичные методы класса

cache_fixtures(connection, fixtures_map) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 556
def cache_fixtures(connection, fixtures_map)
  cache_for_connection(connection).update(fixtures_map)
end
cache_for_connection(connection) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 540
def cache_for_connection(connection)
  @@all_cached_fixtures[connection]
end
cached_fixtures(connection, keys_to_fetch = nil) Показать исходный код
# 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
composite_identify(label, key) Показать исходный код
# 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 }
context_class() Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 623
def context_class
  @context_class ||= Class.new
end

Базовый класс для контекстов вычисления, используемых фикстурами ERB.

create_fixtures(fixtures_directories, fixture_set_names, class_names = {}, config = ActiveRecord::Base, &block) Показать исходный код
# 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
fixture_is_cached?(connection, table_name) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 544
def fixture_is_cached?(connection, table_name)
  cache_for_connection(connection)[table_name]
end
identify(label, column_type = :integer) Показать исходный код
# 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.

instantiate_all_loaded_fixtures(object, load_instances = true) Показать исходный код
# 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
instantiate_fixtures(object, fixture_set, load_instances = true) Показать исходный код
# 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
new(_, name, class_name, path, config = ActiveRecord::Base) Показать исходный код
# 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
reset_cache() Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 536
def reset_cache
  @@all_cached_fixtures.clear
end

Публичные методы экземпляра

[](x) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 704
def [](x)
  fixtures[x]
end
[]=(k, v) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 708
def []=(k, v)
  fixtures[k] = v
end
each(&block) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 712
def each(&block)
  fixtures.each(&block)
end
size() Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 716
def size
  fixtures.size
end
table_rows() Показать исходный код
# 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.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API