Подтвердить что ты не робот

Стратегия FactoryGirl build_stubbed с ассоциацией has_many

Учитывая стандартное отношение has_many между двумя объектами. Для простого примера отпустите:

class Order < ActiveRecord::Base
  has_many :line_items
end

class LineItem < ActiveRecord::Base
  belongs_to :order
end

Что бы я хотел сделать, так это создать обрезанный заказ со списком оштукатуренных позиций.

FactoryGirl.define do
  factory :line_item do
    name 'An Item'
    quantity 1
  end
end

FactoryGirl.define do
  factory :order do
    ignore do
      line_items_count 1
    end

    after(:stub) do |order, evaluator|
      order.line_items = build_stubbed_list(:line_item, evaluator.line_items_count, :order => order)
    end
  end
end

Вышеприведенный код не работает, потому что Rails хочет вызвать сохранение в порядке, когда назначается строка_имя, и FactoryGirl вызывает исключение: RuntimeError: stubbed models are not allowed to access the database

Итак, как вы (или это возможно) сгенерировать заштрихованный объект, в котором также собрана коллекция has_may?

4b9b3361

Ответ 1

TL; DR

FactoryGirl пытается помочь, сделав очень большое предположение, когда оно создает его "заглушки". А именно: у вас есть id, что означает, что вы не являетесь новой записью и, следовательно, уже сохраняетесь!

К сожалению, ActiveRecord использует это, чтобы решить, должно ли оно сохранить постоянство в актуальном состоянии. Таким образом, заглушенная модель пытается сохранить записи в базе данных.

Пожалуйста, сделайте не попытку прошивки штырей RSpec/mocks на фабриках FactoryGirl. При этом смешиваются две разные философии остолбения на одном и том же объекте. Выбирать один или другой.

RSpec mocks предполагается использовать только в определенных частях спецификации жизненный цикл. Перемещение их в factory устанавливает среду, которая будет скрыть нарушение дизайна. Ошибки, возникающие в результате этого, будут путают и трудно отследить.

Если вы посмотрите на документацию, включающую RSpec, чтобы сказать тест/блок, вы можете видеть, что он предоставляет методы для обеспечения правильности работы установить и разорвать между испытаниями. Помещение издевательств на фабрики не дает такой гарантии, что это произойдет.

Здесь есть несколько вариантов:

  • Не используйте FactoryGirl для создания заглушек; используйте библиотеку stubbing (rspec-mocks, minitest/mocks, mocha, flexmock, rr и т.д.)

    Если вы хотите, чтобы ваша логика атрибутов модели в FactoryGirl была в порядке. Используйте его для этой цели и создайте заглушку в другом месте:

    stub_data = attributes_for(:order)
    stub_data[:line_items] = Array.new(5){
      double(LineItem, attributes_for(:line_item))
    }
    order_stub = double(Order, stub_data)
    

    Да, вам нужно вручную создавать ассоциации. Это неплохая вещь, см. ниже для дальнейшего обсуждения.

  • Очистите поле id

    after(:stub) do |order, evaluator|
      order.id = nil
      order.line_items = build_stubbed_list(
        :line_item,
        evaluator.line_items_count,
        order: order
      )
    end
    
  • Создайте собственное определение new_record?

    factory :order do
      ignore do
        line_items_count 1
        new_record true
      end
    
      after(:stub) do |order, evaluator|
        order.define_singleton_method(:new_record?) do
          evaluator.new_record
        end
        order.line_items = build_stubbed_list(
          :line_item,
          evaluator.line_items_count,
          order: order
        )
      end
    end
    

Что здесь происходит?

IMO, обычно не рекомендуется пытаться создать "заштрихованную" has_many связь с FactoryGirl. Это, как правило, приводит к более тесно связанному коду и потенциально много вложенных объектов бесполезно создаются.

Чтобы понять эту позицию и что происходит с FactoryGirl, нам нужно взгляните на несколько вещей:

  • Уровень сохранности базы данных /gem (т.е. ActiveRecord, Mongoid, DataMapper, ROM и т.д.)
  • Любые библиотеки-заглушки/mocking (mintest/mocks, rspec, mocha и т.д.)
  • Назначение mocks/stubs

Уровень сохранения базы данных

Каждый уровень сохранности базы данных ведет себя по-разному. Фактически, многие ведут себя по-разному между основными версиями. FactoryGirl пытается не делать предположений о том, как этот слой настроен. Это дает им максимальную гибкость в долгое время.

Предположение: Я предполагаю, что вы используете ActiveRecord для остальной части это обсуждение.

По моему написанию, текущая версия GA ActiveRecord равна 4.1.0. когда вы устанавливаете на нем ассоциацию has_many, есть a lot который идет on.

Это также немного отличается в более старых версиях AR. Это очень отличается в Mongoid и т.д. Не разумно ожидать, что FactoryGirl поймут тонкости всех этих драгоценных камней, а также различия между версиями. Это просто так происходит, что писатель ассоциации has_many попытки сохранить постоянство в актуальном состоянии.

Возможно, вы думаете: "но я могу установить обратный с заглушкой"

FactoryGirl.define do
  factory :line_item do
    association :order, factory: :order, strategy: :stub
  end
end

li = build_stubbed(:line_item)

Да, это правда. Хотя это просто потому, что AR решил не сохранить. Оказывается, это поведение - это хорошо. В противном случае это было бы очень трудно настроить временные объекты, не часто попадая в базу данных. Кроме того, он позволяет сохранять несколько объектов в одном транзакции, отбросив всю транзакцию, если возникла проблема.

Теперь вы можете подумать: "Я полностью могу добавить объекты к has_many без попадание в базу данных "

order = Order.new
li = order.line_items.build(name: 'test')
puts LineItem.count                   # => 0
puts Order.count                      # => 0
puts order.line_items.size            # => 1

li = LineItem.new(name: 'bar')
order.line_items << li
puts LineItem.count                   # => 0
puts Order.count                      # => 0
puts order.line_items.size            # => 2

li = LineItem.new(name: 'foo')
order.line_items.concat(li)
puts LineItem.count                   # => 0
puts Order.count                      # => 0
puts order.line_items.size            # => 3

order = Order.new
order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") }
puts LineItem.count                   # => 0
puts Order.count                      # => 0
puts order.line_items.size            # => 5

Да, но здесь order.line_items действительно ActiveRecord::Associations::CollectionProxy. Он определяет его собственный build, #<<, и #concat методы. Конечно, они действительно все делегируют обратно в определенную ассоциацию, которые для has_many являются эквивалентными методами: ActiveRecord::Associations::CollectionAssocation#build и ActiveRecord::Associations::CollectionAssocation#concat. Они учитывают текущее состояние экземпляра базовой модели, чтобы чтобы решить, следует ли упорствовать сейчас или позже.

Все, что FactoryGirl действительно может сделать, это позволить поведению базового класса определите, что должно произойти. Фактически это позволяет использовать FactoryGirl для создать любой класс, а не просто модели базы данных.

FactoryGirl пытается немного помочь с сохранением объектов. Это в основном на стороне create заводов. За свою страницу вики взаимодействие с ActiveRecord:

... [a factory] сначала сохраняет ассоциации, так что внешние ключи будут правильно установлены на зависимые модели. Чтобы создать экземпляр, он вызывает новое без каких-либо аргументы, присваивает каждому атрибуту (включая ассоциации), а затем вызывает спасти!. factory_girl не делает ничего особенного для создания ActiveRecord экземпляров. Он не взаимодействует с базой данных или не расширяет ActiveRecord или ваши модели в любом случае.

Подождите! Возможно, вы заметили, что в приведенном выше примере я пропустил следующее:

order = Order.new
order.line_items = Array.new(5){ |n| LineItem.new(name: "test#{n}") }
puts LineItem.count                   # => 0
puts Order.count                      # => 0
puts order.line_items.size            # => 5

Да, это правильно. Мы можем установить order.line_items= в массив, и это не сохранялось! Итак, что дает?

Библиотека Stubbing/Mocking

Существует много разных типов, и FactoryGirl работает с ними всеми. Зачем? Потому что FactoryGirl ничего не делает с кем-либо из них. Это полностью не знают о какой библиотеке у вас есть.

Помните, что вы добавляете синтаксис FactoryGirl в свою тестовую библиотеку . Вы не добавляете свою библиотеку в FactoryGirl.

Итак, если FactoryGirl не использует вашу предпочитаемую библиотеку, что она делает?

Назначение Mocks/Stubs

Прежде чем мы перейдем к деталям капота, нам нужно определить что a "заглушка" есть и его цель:

Stubs предоставляют консервированные ответы на вызовы, сделанные во время теста, обычно не вообще не реагируя на что-либо вне того, что запрограммировано для теста. Опоздатели могут также записывать информацию о вызовах, таких как заглушка шлюза электронной почты который запоминает сообщения, которые он отправил, или, может быть, только то, сколько сообщений оно 'Послал'.

это тонко отличается от "макета":

Mocks...: объекты, предварительно запрограммированные с ожиданиями, которые образуют спецификация вызовов, которые они должны получать.

Штыри служат способом установки коллаборационистов с законченными ответами. Придерживаться только открытый API-интерфейс соавторов, который вы касаетесь для конкретного теста, окурки легкие и маленькие.

Без какой-либо библиотеки "stubbing" вы можете легко создать свои собственные заглушки:

stubbed_object = Object.new
stubbed_object.define_singleton_method(:name) { 'Stubbly' }
stubbed_object.define_singleton_method(:quantity) { 123 }

stubbed_object.name       # => 'Stubbly'
stubbed_object.quantity   # => 123

Так как FactoryGirl полностью агностик, когда дело доходит до их "заглушки", это подход, который они берут.

Глядя на реализацию FactoryGirl v.4.4.0, мы видим, что следующие методы будут затушеваны, если вы build_stubbed:

  • persisted?
  • new_record?
  • save
  • destroy
  • connection
  • reload
  • update_attribute
  • update_column
  • craeted_at

Все это очень ActiveRecord-y. Однако, как вы видели с помощью has_many, это довольно непроницаемая абстракция. Общая площадь API API ActiveRecord очень большой. Не совсем разумно ожидать, что библиотека полностью его охватит.

Почему ассоциация has_many не работает с заглушкой FactoryGirl?

Как отмечалось выше, ActiveRecord проверяет его состояние, чтобы решить, должно ли оно сохранить постоянство в актуальном состоянии. Из-за пронумерованного определения new_record? установка любого has_many приведет к действию базы данных.

def new_record?
  id.nil?
end

Прежде чем выбросить некоторые исправления, я хочу вернуться к определению stub:

Штыри предоставляют консервированные ответы на вызовы, сделанные во время теста, обычно не вообще не реагируя на что-либо вне того, что запрограммировано для теста. Опоздатели могут также записывать информацию о вызовах, таких как заглушка шлюза электронной почты который запоминает сообщения, которые он отправил, или, может быть, только то, сколько сообщений оно 'Послал'.

Выполнение FactoryGirl заглушки нарушает этот принцип. Поскольку он не имеет идея, что вы собираетесь делать в своем тесте/спецификации, просто пытается запретить доступ к базе данных.

Исправить # 1: не использовать FactoryGirl для создания заглушек

Если вы хотите создать/использовать заглушки, используйте библиотеку, предназначенную для этой задачи. поскольку кажется, вы уже используете RSpec, используйте его double (и новую проверку instance_double, class_double, а также object_double в RSpec 3). Или используйте Mocha, Flexmock, RR или что-то еще.

Вы даже можете свернуть свой собственный супер простой заглушка factory (да, есть проблемы с это, это просто пример простого способа создания объекта с консервами ответы):

require 'ostruct'
def create_stub(stubbed_attributes)
  OpenStruct.new(stubbed_attributes)
end

FactoryGirl упрощает создание 100 объектов модели, когда вы действительно необходимо 1. Конечно, это ответственный вопрос использования; как всегда приходит великая сила создавать ответственность. Просто очень легко упустить глубоко вложенные ассоциации, которые на самом деле не принадлежат к заглушке.

Кроме того, как вы заметили, абстракция FactoryGirl "stub" немного неясно, заставляя вас понимать как его реализацию, так и вашу базу данных внутренности внутреннего слоя. Использование stubbing lib должно полностью освободить вас от этой зависимости.

Если вы хотите, чтобы ваша логика атрибутов модели в FactoryGirl была в порядке. Используйте его для этой цели и создайте заглушку в другом месте:

stub_data = attributes_for(:order)
stub_data[:line_items] = Array.new(5){
  double(LineItem, attributes_for(:line_item))
}
order_stub = double(Order, stub_data)

Да, вам нужно вручную настроить ассоциации. Хотя вы только настраиваете те ассоциации, которые вам нужны для теста/спецификации. Вы не получаете 5 других которые вам не нужны.

Это одно дело, что наличие реальной библиотеки stubbing помогает явно понять. Это ваши тесты/спецификации, дающие вам отзывы о ваших вариантах дизайна. С как это, читатель спецификации может задать вопрос: "Зачем нам нужно 5 ли позиции? "Если это важно для спецификации, здорово это прямо там впереди и очевидно. В противном случае его не должно быть.

То же самое относится к тем длинным цепочкам методов, которые называются одним объектом, или цепочку методов на последующих объектах, возможно, пора остановиться. закон demeter, чтобы помочь вы не мешаете вам.

Исправить # 2: очистить поле id

Это скорее хак. Мы знаем, что по умолчанию stub устанавливает id. Таким образом, мы просто удалите его.

after(:stub) do |order, evaluator|
  order.id = nil
  order.line_items = build_stubbed_list(
    :line_item,
    evaluator.line_items_count,
    order: order
  )
end

У нас никогда не будет заглушки, которая возвращает id И устанавливает a has_many ассоциация. Определение new_record?, что установка FactoryGirl полностью предотвращает это.

Исправить # 3: создать собственное определение new_record?

Здесь мы отделяем понятие id от того, где заглушка является new_record?. Мы вставляем это в модуль, чтобы мы могли повторно использовать его в других местах.

module SettableNewRecord
  def new_record?
    @new_record
  end

  def new_record=(state)
    @new_record = !!state
  end
end

factory :order do
  ignore do
    line_items_count 1
    new_record true
  end

  after(:stub) do |order, evaluator|
    order.singleton_class.prepend(SettableNewRecord)
    order.new_record = evaluator.new_record
    order.line_items = build_stubbed_list(
      :line_item,
      evaluator.line_items_count,
      order: order
    )
  end
end

Нам все равно придется вручную добавить его для каждой модели.

Ответ 2

Я видел, как этот ответ всплывал, но столкнулся с той же проблемой, что и у вас: FactoryGirl: Заполнитель имеет много стратегий сохранения сохранности отношений

Самый чистый способ, который я нашел, - явно запретить также вызовы ассоциации.

require 'rspec/mocks/standalone'

FactoryGirl.define do
  factory :order do
    ignore do
      line_items_count 1
    end

    after(:stub) do |order, evaluator|
      order.stub(line_items).and_return(FactoryGirl.build_stubbed_list(:line_item, evaluator.line_items_count, :order => order))
    end
  end
end

Надеюсь, что это поможет!

Ответ 3

Я нашел решение Bryce самым элегантным, но оно выдает предупреждение об устаревании нового синтаксиса allow().

Чтобы использовать новый синтаксис (чище), я сделал это:

ОБНОВЛЕНИЕ 06/05/2014: мое первое предложение использовало частный метод api, благодаря Aaraon K для более приятного решения, пожалуйста, прочитайте комментарий для дальнейшего обсуждения

#spec/support/initializers/factory_girl.rb
...
#this line enables us to use allow() method in factories
FactoryGirl::SyntaxRunner.include(RSpec::Mocks::ExampleMethods)
...

 #spec/factories/order_factory.rb
...
FactoryGirl.define do
  factory :order do
    ignore do
      line_items_count 1
    end

    after(:stub) do |order, evaluator|
      items = FactoryGirl.build_stubbed_list(:line_item, evaluator.line_items_count, :order => order)
      allow(order).to receive(:line_items).and_return(items)
    end
  end
end
...