Spec-Zone.ru › Ruby on Rails 8.1

class ActiveRecord::FixtureSet

Родительский класс:
Object

Фикстуры Active Record

Фикстуры позволяют упорядочить данные, с которыми вы хотите проводить тестирование; проще говоря, это примерные данные.

Они хранятся в файлах YAML, по одному файлу на модель. По умолчанию файлы размещаются в папке <your-rails-app>/test/fixtures/ или в папке test/fixtures любого движка вашего приложения.

Путь также можно изменить с помощью ActiveSupport::TestCase.fixture_paths=, если в вашем test_helper.rb уже есть require "rails/test_help".

Файл фикстур имеет расширение .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") %>. Однако эту возможность следует использовать с осторожностью. Смысл фикстур в том, что это стабильные наборы предсказуемых примерных данных. Если вам нужно вставлять динамические значения, возможно, стоит еще раз проверить, насколько хорошо ваше приложение поддается тестированию. Поэтому динамические значения в фикстурах следует считать признаком проблем в коде.

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

reginald: # generated id: 41001176
  name: Reginald the Pirate

Active Record определяет класс модели фикстуры, находит правильный первичный ключ и генерирует его непосредственно перед вставкой фикстуры в базу данных.

Для заданной метки генерируется постоянный идентификатор, поэтому мы можем узнать идентификатор любой фикстуры, не загружая ее, если знаем ее метку.

Ссылки по меткам для ассоциаций (, , )

Указывать внешние ключи в фикстурах может быть ненадежно и, кроме того, затрудняет чтение. Поскольку Active Record может определить идентификатор любой фикстуры по ее метке, вместо идентификатора можно указать внешний ключ по метке.

Добавим еще несколько обезьян и пиратов.

### 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: george), а не целевой идентификатор для внешнего ключа (monkey_id: 1).

Полиморфная

Поддержка полиморфных связей несколько сложнее, поскольку 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_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 576
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 560
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 568
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 633
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 641
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 595
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 564
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 619
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 — это хеши SHA-1 версии 5 по RFC 4122.

instantiate_all_loaded_fixtures (object, load_instances = true) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 589
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 580
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 713
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 556
def reset_cache
  @@all_cached_fixtures.clear
end

Открытые методы экземпляра

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