Spec-Zone.ru › Ruby on Rails 4.1

класс ActiveRecord::FixtureSet

Родитель:
Объект

Фикстуры — способ организации данных, с которыми вы хотите провести тестирование; короче говоря, образцовые данные.

Они хранятся в файлах YAML, по одному файлу на модель, которые размещаются в директории, указанной в ActiveSupport::TestCase.fixture_path=(path) (для Rails эта директория настраивается автоматически, поэтому вы можете просто положить свои файлы в <your-rails-app>/test/fixtures/). Файл фикстуры имеет расширение .yml (пример для Rails: <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

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

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

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

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

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

    photo:
      name: kitten.png
      sha: <%= file_sha 'files/kitten.png' %>

Транзакционные фикстуры

Тестовые случаи могут использовать begin+rollback, чтобы изолировать свои изменения в базе данных вместо необходимости удалять+вставлять для каждого тестового случая.

class FooTest < ActiveSupport::TestCase
  self.use_transactional_fixtures = 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 вместо этого.

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

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

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

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

  • Ассоциации HABTM в виде встроенных списков

  • Автоматическое заполнение столбцов timestamp

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

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

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

Константы

MAX_ID

Атрибуты

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

Общедоступные методы класса

cache_fixtures(connection, fixtures_map) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 431
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 415
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 423
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() Показать исходный код

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

# File activerecord/lib/active_record/fixtures.rb, line 556
def self.context_class
  @context_class ||= Class.new
end
create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) { |: connection| ... } Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 494
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

      all_loaded_fixtures.update(fixtures_map)

      connection.transaction(:requires_new => true) do
        fixture_sets.each do |fs|
          conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
          table_rows = fs.table_rows

          table_rows.keys.each do |table|
            conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
          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 419
def self.fixture_is_cached?(connection, table_name)
  cache_for_connection(connection)[table_name]
end
identify(label) Показать исходный код

Возвращает согласованный, независимый от платформы идентификатор для label. Идентификаторы — это положительные целые числа меньше 2^32.

# File activerecord/lib/active_record/fixtures.rb, line 551
def self.identify(label)
  Zlib.crc32(label.to_s) % MAX_ID
end
instantiate_all_loaded_fixtures(object, load_instances = true) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 447
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 435
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 562
def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
  @name     = name
  @path     = path
  @config   = config
  @model_class = nil

  if class_name.is_a?(String)
    ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `FixtureSet.new` will be removed in Rails 4.2. Use the class itself instead.")
  end

  if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
    @model_class = class_name
  else
    @model_class = class_name.safe_constantize if class_name
  end

  @connection  = connection

  @table_name = ( model_class.respond_to?(:table_name) ?
                  model_class.table_name :
                  self.class.default_fixture_table_name(name, config) )

  @fixtures = read_fixture_files path, @model_class
end
reset_cache() Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 411
def self.reset_cache
  @@all_cached_fixtures.clear
end

Общедоступные методы экземпляра

[](x) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 587
def [](x)
  fixtures[x]
end
[]=(k,v) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 591
def []=(k,v)
  fixtures[k] = v
end
each(&block) Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 595
def each(&block)
  fixtures.each(&block)
end
size() Показать исходный код
# File activerecord/lib/active_record/fixtures.rb, line 599
def size
  fixtures.size
end
table_rows() Показать исходный код

Возвращает хеш строк для вставки. Ключ — таблица, значение — список строк для вставки в эту таблицу.

# File activerecord/lib/active_record/fixtures.rb, line 605
def table_rows
  now = config.default_timezone == :utc ? Time.now.utc : Time.now
  now = now.to_s(:db)

  # 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] = label if "$LABEL" == value
      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)
      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.values.each 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.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
              # support polymorphic belongs_to as "label (Type)"
              row[association.foreign_type] = $1
            end

            row[fk_name] = ActiveRecord::FixtureSet.identify(value)
          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–2016 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

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