Spec-Zone.ru › Ruby on Rails 7.2

класс 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 не упорядочены.

Если вам нужны упорядоченные фикстуры, используйте тип 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

Если имена моделей конфликтуют с TestCase методами, вы можете использовать универсальный fixture аксессор

test "generic find" do
  assert_equal "Ruby on Rails", fixture(: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

Иногда вам не так важны данные фикстур, как объём. В таких случаях вы можете смешивать ERB со своими фикстурами YAML, чтобы создать кучу фикстур для нагрузочного тестирования, например:

<% 1.upto(1000) do |i| %>
fix_<%= i %>:
  id: <%= i %>
  name: guy_<%= i %>
<% end %>

Это создаст 1000 очень простых фикстур.

Используя ERB, вы также можете внедрять динамические значения в свои фикстуры с вставками, подобными <%= Date.today.strftime("%Y-%m-%d") %>. Однако эта функция должна использоваться с осторожностью. Цель фикстур — быть стабильными единицами предсказуемых образцов данных. Если вам нужно внедрять динамические значения, возможно, вам следует пересмотреть, правильно ли ваш application тестуем. Поэтому динамические значения в фикстурах следует рассматривать как признак плохой архитектуры.

Вспомогательные методы, определённые в фикстуре, не будут доступны в других фикстурах, чтобы предотвратить нежелательные межтестовые зависимости. Методы, используемые несколькими фикстурами, должны быть определены в модуле, который включён в 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).

Полиморфный 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

Давайте уберём фикстуру 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 ассоциации.

Автозаполнение столбцов со временем

Если ваша таблица/модель указывает какие-либо стандартные столбцы со временем 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, foreign_key: [:shop_id, :order_id]
  belongs_to :book, foreign_key: [: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_pool, fixtures_map) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 578
def cache_fixtures(connection_pool, fixtures_map)
  cache_for_connection_pool(connection_pool).update(fixtures_map)
end
cache_for_connection_pool(connection_pool) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 562
def cache_for_connection_pool(connection_pool)
  @@all_cached_fixtures[connection_pool]
end
cached_fixtures(connection_pool, keys_to_fetch = nil) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 570
def cached_fixtures(connection_pool, keys_to_fetch = nil)
  if keys_to_fetch
    cache_for_connection_pool(connection_pool).values_at(*keys_to_fetch)
  else
    cache_for_connection_pool(connection_pool).values
  end
end
composite_identify(label, key) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 635
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 643
def context_class
  @context_class ||= Class.new
end

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

create_fixtures(fixtures_directories, fixture_set_names, class_names = {}, config = ActiveRecord::Base) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 597
def create_fixtures(fixtures_directories, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
  fixture_set_names = Array(fixture_set_names).map(&:to_s)
  class_names.stringify_keys!

  connection_pool = config.connection_pool
  fixture_files_to_read = fixture_set_names.reject do |fs_name|
    fixture_is_cached?(connection_pool, fs_name)
  end

  if fixture_files_to_read.any?
    fixtures_map = read_and_insert(
      Array(fixtures_directories),
      fixture_files_to_read,
      class_names,
      connection_pool,
    )
    cache_fixtures(connection_pool, fixtures_map)
  end
  cached_fixtures(connection_pool, fixture_set_names)
end
fixture_is_cached?(connection_pool, table_name) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 566
def fixture_is_cached?(connection_pool, table_name)
  cache_for_connection_pool(connection_pool)[table_name]
end
identify(label, column_type = :integer) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 621
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 591
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 582
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 715
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 558
def reset_cache
  @@all_cached_fixtures.clear
end

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

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