Factory_bot: How Factory Girl create association with existing object?

Created on 22 Aug 2014  路  27Comments  路  Source: thoughtbot/factory_bot

I have two associated models.

class Girl < ActiveRecord::Base
   belongs_to :boy
end
class Boy < ActiveRecord::Base
   has_one :girl
end

And I define factory girl for them:

factory :girl do
  boy
  name 'saria'
end

factory :boy do
  name do
    puts create boy'
   'leo'
  end
end

Then i create a girl with an exsisting boy:

boy = create :boy
girl = create :girl, boy: boy

I will get following result:

>>>create boy
>>>create boy

I am wondering why the block of name of boy runs twice.
And, if i put code that can only runs once, troubles come.

Version of my factory_girl:

  • factory_girl (4.4.0)
  • factory_girl_rails (4.4.1)

Most helpful comment

@Euraldius, both Boy and Girl are "objects", as in Ruby objects. Let's not make such a big deal out of nothing, really, it's just an example. Nobody questioned or undermined human rights or the equality of genders.

All 27 comments

I have the same issue or question.
In my case this leads to a huge amount of time spent invoking the factory twice.
Only half of the objects are actually used, because one of the two created objects are discarded anyway.

Is there any way to prevent this from happening?
It would save a lot of time in my test suite.

+1, same question as this makes difficult for me to test some things and this is not documented

Ran into a similar problem where we had two associations attempting to create unique parent instances when we wanted them to belong to the same parent.

Class Term < ActiveRecord::Base
  has_many :candidates
  has_many :periods
end

Class Period < ActiveRecord::Base
  belongs_to :term
end

Class Candidate < ActiveRecord::Base
  belongs_to :term
end

In order to avoid needing to make multiple factory calls from the spec to reach the same term we ended up defining the factories like this:

factory :period do
  term { Term.first || association(:term) }
end

factory :candidate do
  term { Term.first || association(:term) }
end

This way the pre-existing Term can be referred to instead of creating two instances.

Calling Thing.first introduces non-determinism in the test suite, since records can be created at various layers without the developer even being aware. I'd recommend creating a record and assigning it directly if the intent is to share the value.

@joshuaclayton is there any chance that you could give some practical example to how to do that?

@scaryguy sure thing!

term = create(:term)
period = create(:period, term: term)
candidate = create(:candidate, term: term)

Thank you @joshuaclayton !

From OP:

class Girl < ActiveRecord::Base
   belongs_to :boy
end
class Boy < ActiveRecord::Base
   has_one :girl
end

For real? Girl belongs to boy didn't feel a little gross to anyone?

Let's write examples without reducing women to objects, please, and call this stuff out when we see it.

@Euraldius, both Boy and Girl are "objects", as in Ruby objects. Let's not make such a big deal out of nothing, really, it's just an example. Nobody questioned or undermined human rights or the equality of genders.

Bumping this, OPs question still isn't answered. In the original post, OP is already using the suggestion provided here.

The only solution I can come up with is to say (in the context of OP example)

factory :girl do
  boy { |weird_thing| somehow_inspect_weird_thing_to_check_if_boy_was_already_provided(weird_thing) }
  name 'saria'
end

But this doesnt seem very idiomatic.

Also, at least where I live, there is a a constant, subtle, barrage of messaging coming from the TV and other sources telling us that women are the property of men. This is insidious, and FWIW, I think we should try to be conscious not to take part. cc @linkyndy.

just use .where(name: 'James').first_or_create!(foo: bar) in the block

so you could have something like

Class Term < ActiveRecord::Base
  has_many :candidates
  has_many :periods
end

Class Period < ActiveRecord::Base
  belongs_to :term
end

Class Candidate < ActiveRecord::Base
  belongs_to :term
end

Then do something like

factory :period do
  term { Term.where(name: 'Term 1').first_or_create!  }
end

@linkyndy Do you seriously think @Euraldius did not understand that those labels referred to Ruby objects? Let me slowly explain to you the point that was being made:

When engineers use labels such as "boy"/"girl" in ways that reinforce toxic gender stereotypes, such engineers show their ignorance and (perhaps unconscious, perhaps latent) misogyny. So, stop. Think about how you use your language. And be considerate.

Another related example: the widespread use of repulsive "master"/"slave" labels in software systems.

Just fucking think about it for a second... bro.

https://github.com/thoughtbot/factory_bot/pull/943

https://github.com/thoughtbot/factory_bot/pull/1051

Bad example or not, the issue has been handled on this and many other levels.

We still haven't really found a great solution to the original problem, but if the only submissions here are going to be in regards to the social issues involved, then the should be locked. @Euraldius @joshuaclayton

I solved this problem by using :inverse_of on the association in both models.

class Belonging < ActiveRecord::Base
   belongs_to :having, inverse_of: :belonging
end
class Having < ActiveRecord::Base
   has_one :belonging, inverse_of: :having
end



md5-3a866b8895ebdb3aec75226965bcfe87



factory :belonging do
  having
end



md5-044b633b3924251b65cad6759c7a1d87



factory :having do
  after(:create) do |having, evaluator|
    having.belonging || create(:belonging, having: having)
  end
end

In the callback, inverse_of allows for the associated Belonging to be returned from memory even if it isn't saved yet, as is the case when you create :belonging. You can do some dynamic checking, while not relying on something like Belonging.first.

@Kriechi this will help your test performance by not creating extra throwaway records, and also affect your actual production performance by making fewer trips to the db. Which is the actual purpose of inverse_of.

What do you think about:

factory :singleton do
  initialize_with do
      Singleton.where(
        name: 'unique_name'
      ).first_or_initialize
  end
end

this moves the logic to the definition of your singleton factory.

If you're really working with a Singleton, I'm not sure you need FactoryBot at all. If it's not really a singleton, than first_or_initialize has the non-determinism problem mentioned above. Am I missing something about what you're trying to do?

I feel hurt by the word Singleton because it reminds me of who I think I am.

This message is not trollish intent to hurt anyone, rather a part of a philosophical debate. (cc @skatenerd)

I wholeheartedly agree that we should fight any discrimination.

@mib32 sry i got a bit defensive, seeing how the original namechange discussion ... turned out

@drewcimino The requirement in my case was that there is only one entry with a specific name in the db table. Every reference to a :singleton with the same name should be the same instance.
first_or_initialize is not atomic and could lead to race conditions. My tests run single threaded so there shouldn't be any race condition. Anyway the unique condition would be enforced on database level with a unique index, but a race condition would break the test ...

i can see how the girl-belongs-to-boy rubs the wrong way, linkyndy. imagine a similar example, only swap the Girl model to Black and Boy model to White.

@adler99 So it seems your Singleton class isn't really a singleton, but does have relatively few saved records, like some sort of reference data. If the objects of this class are _that_ standardized - that is, a record with a given name always has the same associated data - I would create them at the beginning of the test suite with a seeds-file-like script, and building some class methods for accessing them.

Something like:

Singleton.drewcimino
=> #<Singleton id: 1, name: "drewcimino">

as opposed to:

Singleton.where(name: "drewcimino").first_or_initialize
=> #<Singleton id: 1, name: "drewcimino"> OR #<Singleton id: nil, name: "drewcimino">

Using the term, period and candidate example, if I add another layer, say:

Class Bear < ActiveRecord::Base
  belongs_to :candidate
  belongs_to :period
end

Is it possible to create the term inside of the bear FactoryBot beforehand and then get candidate and period to use the bear to create internally?

Probably a wrong example here but is it possible to do it inside (sometimes an object belongs to an object that belongs to an object, and if we were to manually create each object from the top and reference the next one outside of the factorybot file then it would be a mess everywhere), something like this:

# this is wrong but you get the idea
FactoryBot.define do
  factory :bear do
    # Run this first so everytime a bear gets created, a term for this bear gets created first to be used later
    before(:create) { create(:term }

    # Using the term created before this
    association :period, factory: :period, term: term
    association :candidate, factory: :candidate, term: term
  end
end

@CyberMew If you want to complete the period and candidate with Bear#term after Bear/Term are created, I think an after block is the prescription.

FactoryBot.define do
  factory :bear do
    association :term
    association :period
    association :candidate

    after(:build) do |bear|
      bear.period.term ||= bear.term
      bear.candidate.term ||= bear.term
    end
  end
end

I use after(:build) instead of after(:create) and AR methods instead of passing ids around, so this setup will work for both build(:bear) and create(:bear). The associations follow the build strategy (build/create) of the parent, so if you create they'll assign first and then automatically populate all the ids on save.

You can force build strategies if you want to, but I don't recommend it as you can easily end up with some funky saved/not-saved "associated" records. But there's a bit on that here: https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#associations

I came here for code and got distracted by social issues. I don't have a problem with discussing our society. In fact, I'd like to know, who decides which social issues are worth discussing while working? Is it as simple as me labeling my issue an injustice - then I proceed to make my case? Currently, I have access to this comment box, so I suppose I have an equal right to say - but can someone point me to the rules?

I opted for before(:create)

FactoryBot.define do
  factory :album do
    name { Faker::Music::album }
    year { rand(1887..Time.new.year).to_i }

    before(:create) do |album|
      album.artist = create(:artist)
      album.genre = create(:genre)
    end
  end
end

I believe we need many-to-many association between Boy and Girl classes.

Came here because of a frustrating engineering problem, disappointed to see people are discussing irrelevant social issues.

  • Yes the social issues are important
  • No github issues is probably not the right place to discuss/debate them
  • Call the classes Yob and Lrig, I don't care, I need a solution to the inter-dependent factory objects

As a reminder, anybody participating here agrees to follow our Code of Conduct. A code example where a girl belongs to a boy does not follow our Code of Conduct and I appreciate the folks who called that out.

I'm going to lock this issue. If somebody would be willing to open a new issue with a better example and a summary of the problem and potential solutions offered so far I would be most grateful.

Was this page helpful?
0 / 5 - 0 ratings