Factory_bot: Creating associations initialized via 'build' no longer works in FactoryBot 5.0

Created on 1 Feb 2019  路  14Comments  路  Source: thoughtbot/factory_bot

He guys,

After upgrading to v5.0 of FactoryBot today we started having the following issue.
Our factory is defined like this:

FactoryBot.define do
  factory :task_profile do
    profile
    task { build(:task, account: profile.account) }
  end
end

Where TaskProfile is a model, that belongs_to :profile and belongs_to :task.
This allows us to make sure both profile and task belong to the same account.

So now after we do create(:task_profile), it creates the entry with task_id: nil. Previously it was correctly filled in with id of associated task.

Could you please suggest a fix for the problem? Thank you.

Most helpful comment

@k-rudy got it. So there is something you can do, but it is not currently documented anywhere.

task { association(:task, account: profile.account) } 

I have been thinking about documenting this way of creating associations inside the block, and this is a good case for it. It came up in https://github.com/thoughtbot/factory_bot/issues/1063 as well.

Using association will build the association when you are using FactoryBot.build and create the association when you are using FactoryBot.create. If you want to see the details, the method lives on the FactoryBot::Evaluator class.

All 14 comments

I wasn't able to reproduce this. Can you update this script to produce the error you described?

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails", "5.2.2"
  gem "sqlite3"
  gem "factory_bot", "5.0.0"
end

require "active_record"
require "minitest/autorun"
require "logger"
require "factory_bot"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :task_profiles, force: true do |t|
    t.integer :profile_id
    t.integer :task_id
  end

  create_table :tasks, force: true do |t|
    t.integer :account_id
  end

  create_table :profiles, force: true do |t|
    t.integer :account_id
  end

  create_table :accounts, force: true do |t|
  end
end

class Account < ActiveRecord::Base
end

class Profile < ActiveRecord::Base
  belongs_to :account
end

class Task < ActiveRecord::Base
  belongs_to :account
end

class TaskProfile < ActiveRecord::Base
  belongs_to :profile
  belongs_to :task
end

FactoryBot.define do
  factory :account do
  end

  factory :profile do
    account
  end

  factory :task do
    account
  end

  factory :task_profile do
    profile
    task { build(:task, account: profile.account) }
  end
end

class BugTest < Minitest::Test
  def test_factory_bot_stuff
    task_profile = FactoryBot.create(:task_profile)

    assert task_profile.task_id.present?
  end
end

@composerinteralia I have entered my versions to your script and couldn't reproduce as well. We're using pg v. 0.21.0 as a database adapter and Postgres 9.6 and also Rspec v.3.8.2. Unfortunately pg setup in this script seems a bit more complicated and didn't work for me straight away. I'll try to play with it over the weekend and make it reproducing the issue. Thank you.

Upgrading to factory_bot_rails v5 broke some factories for me too, for what appears to be the same reason.

@composerinteralia, here is the test for the failing scenario. Feel free to uncomment the earlier version of FactoryBot gem to make it passing. Hope it helps

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails", "5.0.7.1"
  gem "sqlite3", '1.3.13'
  gem "factory_bot", "5.0.0"
  # Uncomment to make the test pass
  # gem "factory_bot", "< 5"
end

require "active_record"
require "minitest/autorun"
require "logger"
require "factory_bot"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :task_profiles, force: true do |t|
    t.integer :profile_id
    t.integer :task_id
  end

  create_table :tasks, force: true do |t|
    t.integer :account_id
    t.integer :profile_id
  end

  create_table :profiles, force: true do |t|
    t.integer :account_id
  end

  create_table :accounts, force: true do |t|
  end
end

class Account < ActiveRecord::Base
end

class Profile < ActiveRecord::Base
  belongs_to :account
end

class Task < ActiveRecord::Base
  belongs_to :account
  belongs_to :profile

  validates :account_id, presence: true
end

class TaskProfile < ActiveRecord::Base
  belongs_to :profile
  belongs_to :task
end

FactoryBot.define do
  factory :account do
  end

  factory :profile do
    account
  end

  factory :task do
    profile
    account { profile.account }
  end

  factory :task_profile do
    profile
    task { build(:task) }
  end
end

class BugTest < Minitest::Test
  def test_factory_bot_stuff
    task_profile = FactoryBot.create(:task_profile)
    assert task_profile.task_id.present?
  end
end

Thanks so much! I'll try to take a proper look at this before the end of the week, but based on a quick glance seeing validates :account_id, presence: true, I am thinking this is similar to https://github.com/thoughtbot/factory_bot_rails/issues/314.

A temporary workaround would be to set FactoryBot.use_parent_strategy = false (we changed the default value for that configuration option). But I will see if I can come up with another solution that works with the new default for use_parent_strategy.

Just for additional context on this issue: The upgrade is breaking a quarter of spec suites according to meta analysis by depend-a-bot
https://dependabot.com/compatibility-score/?dependency-name=factory_bot_rails&package-manager=bundler&previous-version=4.11.1&new-version=5.0.0

I'm also seeing something that I think is related to this.

Instead of doing the following:

factory :service
  product # Service has a many_to_one relationship with Product
end

factory :product
  name { 'Product' }
end

I have to do

factory :service
  product { create(:product) } # Service has a many_to_one relationship with Product
end

factory :product
  name { 'Product' }
end

Edit: Setting FactoryBot.use_parent_strategy = false resolves it, though.

@pboling thank you for that information. That makes sense, since changing the default value for FactoryBot.use_parent_strategy was an intentional breaking change.

@jrgns I'm glad you were able to set FactoryBot.use_parent_strategy = false to go back to the old behavior and get your test suite passing again. If at some point you want to try out the new behavior, instead of updating the product association in the service factory, it might be worth looking at the specific tests that are failing (if there aren't too many of them). In places that you are using FactoryBot.build(:service) you may need to switch to FactoryBot.create(:service) instead.

@k-rudy your issue might be a little bit different. I still haven't had a chance to look into it too closely, but I will try to take a look soon!

@k-rudy I was able to get your test passing again by changing the task_profile factory from:

  factory :task_profile do
    profile
    task { build(:task) }
  end

to:

  factory :task_profile do
    profile
    task
  end

By letting task be an association instead of a regular attribute, factory_bot will know to create the associated task when you call FactoryBot.create(:task_profile).

What you have before used to work because although you were using the build strategy for the task, its associated profile and account were still constructed using the create strategy. factory_bot 5 changed the default for use_parent_strategy to true, thus causing the build strategy to build associations, rather than create them. building the associates doesn't quite work here, since you are validating the presence of an id.


Another option is to change back to the old behavior for now by setting FactoryBot.use_parent_strategy = false.

@composerinteralia thanks for the info, in the test I slightly simplified the factory, as it originally was:

factory :task_profile do
  profile
  task { build(:task, account: profile.account) }
end

to make sure when we don't pass the task explicitly, it is created for the same account as profile. From what you have said - seems like it will no longer be possible in 5.0, unless we change the use_parent_strategy option?

@k-rudy got it. So there is something you can do, but it is not currently documented anywhere.

task { association(:task, account: profile.account) } 

I have been thinking about documenting this way of creating associations inside the block, and this is a good case for it. It came up in https://github.com/thoughtbot/factory_bot/issues/1063 as well.

Using association will build the association when you are using FactoryBot.build and create the association when you are using FactoryBot.create. If you want to see the details, the method lives on the FactoryBot::Evaluator class.

@composerinteralia the proposed solution to replace build with association works like a charm for us. This is how it looks at the end:

FactoryBot.define do
  factory :task_profile do
    profile
    task { association(:task, account: profile.account) }
  end
end

Evaluator#association is now officially documented: https://github.com/thoughtbot/factory_bot/issues/1268#issuecomment-656226689

Was this page helpful?
0 / 5 - 0 ratings