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.
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.
This may also be related to https://github.com/thoughtbot/factory_bot_rails/issues/314
@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
Most helpful comment
@k-rudy got it. So there is something you can do, but it is not currently documented anywhere.
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
associationwillbuildthe association when you are usingFactoryBot.buildandcreatethe association when you are usingFactoryBot.create. If you want to see the details, the method lives on theFactoryBot::Evaluatorclass.