Проблема с Factory_girl, ассоциацией и after_initialize

У меня есть семейный класс, определенный так:

class Family < ActiveRecord::Base
  after_initialize :initialize_family
  belongs_to :user
  validates :user, 
       :presence => true

  validates :name,   
       :presence => true,          
       :length => { :maximum => 30 },
       :format => { :with => /\A[a-zA-Z0-9\-_\s\']+\z/i}

  def initialize_family
    if self.name.blank? && self.user
        self.name = "#{self.user.profile_full_name}'s Family"
    end
  end
end

У меня в factory.rb есть:

Factory.define :family do |f|
   f.association :user, :factory => :user
end

В моем family_spec.rb у меня есть

let(:family) { Factory(:family) }

Но это не удается с:

1) Family is valid with valid attributes
     Failure/Error: let(:family) { Factory(:family) }
     ActiveRecord::RecordInvalid:
       Validation failed: Name can't be blank, Name is invalid, Languages can't be blank, Languages is too short (minimum is 1 characters)
     # ./spec/models/family_spec.rb:8:in `block (2 levels) in <top (required)>'
     # ./spec/models/family_spec.rb:10:in `block (2 levels) in <top (required)>'

Используя отладчик, я вижу, что когда after_initialize вызывается, self.user равен нулю. Почему это происходит? Если я вызываю семью с помощью create или new, все работает нормально.

Спасибо за любую помощь.


person Matteo Melani    schedule 06.05.2011    source источник


Ответы (3)


Вот ответ, который я получил от Джо Ферриса:

factory_girl не передает аргументы конструктору. Он использует #user= в вашей модели и создает ее экземпляр без каких-либо аргументов.

и это от Бена Хьюза:

Чтобы уточнить, что говорит Джо, методы after_initialize вызываются сразу после инициализации объекта, и это время действительно не установлено пользователем.

Так, например, пока это будет работать:

family = Family.create!(:user => @user) # or @user.families.create ...

Этого не будет (что и делает factory_girl под капотом):

family = Family.new
family.user = @user
family.save!

В общем, вы должны быть очень осторожны, используя after_initialize, так как помните, что это вызывается при каждой инициализации объекта. Вызов Family.all для 1000 объектов вызовет 1000 вызовов.

Похоже, в этом случае вам лучше использовать before_validation вместо after_initialize.

Следующий синтаксис также подходит для тестирования в rspec:

let (:family) { Family.create(:user => @user) }
person Matteo Melani    schedule 09.05.2011
comment
Хорошо, это действительно помогло. Но это запутанно :*( Извините за плохой английский - person woto; 03.12.2012
comment
Получали ли они какие-либо преимущества, не создавая экземпляры новых объектов со своими аргументами? Как говорит @GuiGS ниже, можно использовать initialize_with, так что это, конечно, не ограничение дизайна factory_girl. Интересно, почему поведение initialize_with не является поведением по умолчанию? - person Scott W; 26.07.2015

Поскольку after_initialize запускается после создания новых объектов, а factory_girl создает экземпляры, вызывая new по умолчанию без каких-либо аргументов, вы должны использовать initialize_with, чтобы перезаписать сборку по умолчанию.

FactoryGirl.define do
  factory :family do
    initialize_with { new(user: build(:user)) }
  end
end
person GuiGS    schedule 20.01.2015
comment
В то время как ответ Маттео отвечает, почему, это фактическое, как. - person Drew Verlee; 07.07.2015
comment
Его также можно назвать так initialize_with { new(attributes) } - person Danieth; 06.08.2015
comment
Это действительно помогло! Спасибо :) - person Joe; 29.05.2018

Я считаю, что это потому, что ассоциация ленивая, поэтому в «after_initialize» еще нет пользователя.

http://rdoc.info/github/thoughtbot/factory_girl/v1.3.3/file/README.rdoc

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

f.user Factory(:user)
person Roman    schedule 08.05.2011