Spec-Zone.ru › Ruby on Rails 5.0

класс 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" do
  assert_equal "Ruby on Rails", 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 с файлами фикстур 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.rb`

    module 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 !Foo.all.empty?
    Foo.destroy_all
    assert Foo.all.empty?
  end

  test "godzilla aftermath" do
    assert !Foo.all.empty?
  end
end

Если вы предварительно загружаете свою тестовую базу данными всех данных фикстур (вероятно, в задаче rake) и используете транзакционные тесты, то вы можете опустить все объявления фикстур в своих тестовых случаях, поскольку все данные уже там, и каждый случай откатывает свои изменения.

Для использования созданных экземпляров фикстур с предварительно загруженными данными установите self.pre_loaded_fixtures в значение true. Это обеспечит доступ к данным фикстуры для каждой таблицы, которая была загружена с помощью фикстур (в зависимости от значения use_instantiated_fixtures).

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

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

  2. Ваша база данных не поддерживает транзакции. Каждая база данных Active Record поддерживает транзакции, за исключением MySQL MyISAM. Используйте InnoDB, MaxDB или NDB вместо неё.

Расширенные фикстуры

Фикстуры, которые не указывают ID, получают дополнительные функции:

  • Стабильные автоматически сгенерированные ID

  • Ссылки на метки для ассоциаций (belongs_to, has_one, has_many)

  • HABTM ассоциации как списки в строке

Есть ещё некоторые расширенные возможности, доступные даже если ID указан:

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

  • Интерполяция меток фикстуры

  • Поддержка значений по умолчанию 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 ассоциации и позволяет указать целевую метку для ассоциации (обезьяна: джордж) вместо целевого 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 ассоциации.

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

Если ваша таблица/модель определяет какие-либо стандартные столбцы временных меток 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

Атрибуты

config[Ч]
fixtures[Ч]
model_class[Ч]
name[Ч]
table_name[Ч]

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

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

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

create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) { |: connection| ... } Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 511
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?
    connection.disable_referential_integrity do
      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

      connection.transaction(requires_new: true) do
        deleted_tables = Hash.new { |h, k| h[k] = Set.new }
        fixture_sets.each do |fs|
          conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
          table_rows = fs.table_rows

          table_rows.each_key do |table|
            unless deleted_tables[conn].include? table
              conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete"
            end
            deleted_tables[conn] << table
          end

          table_rows.each do |fixture_set_name, rows|
            rows.each do |row|
              conn.insert_fixture(row, fixture_set_name)
            end
          end

          # Cap primary key sequences to max(pk).
          if conn.respond_to?(:reset_pk_sequence!)
            conn.reset_pk_sequence!(fs.table_name)
          end
        end
      end

      cache_fixtures(connection, fixtures_map)
    end
  end
  cached_fixtures(connection, fixture_set_names)
end
fixture_is_cached?(connection, table_name) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 442
def self.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 572
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 хешами.

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

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

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

Spec-Zone.ru

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