thoughtbot / factory_bot

A library for setting up Ruby objects as test data.

Home Page:https://thoughtbot.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Setting primary key when building model doesn't set associated primary key

ryanmk54 opened this issue · comments

Description

We have two tables where the primary key is generated using the same sequence. I need to set the primary key value when building/creating a model from FactoryBot, because we have an id hard-coded in the code we are testing.

If I set the primary key when calling create(:factory_name, primary_key: 5), the association is still created, but it isn't tied to the parent object, because the child object was created with the default values, not the parent's primary key.

Reproduction Steps

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"
  git_source(:github) { |repo| "https://github.com/#{repo}.git" }
  gem "factory_bot", "~> 6.0"
  gem "activerecord"
  gem "sqlite3"
end

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

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :vendors, primary_key: :vendor_no
  create_table :vendor_items, primary_key: :vendor_no
end

class Vendor < ActiveRecord::Base
  self.primary_key = :vendor_no
  belongs_to :vendor_item, foreign_key: :vendor_no, primary_key: :vendor_no
end

class VendorItem < ActiveRecord::Base
  self.primary_key = :vendor_no
end

FactoryBot.define do
  factory :vendor do
    vendor_item

    # Possible work-around
    # after(:build) do |vendor|
    #   vendor.vendor_item = build(:vendor_item, vendor_no: vendor.vendor_no)
    # end
  end

  factory :vendor_item do
  end
end

class FactoryBotTest < Minitest::Test
  def test_factory_bot_stuff
    vendor = FactoryBot.create(:vendor, vendor_no: 5)

    assert_instance_of(VendorItem, vendor.vendor_item)
  end
end

# Run the tests with `ruby <filename>`

Expected behavior

I thought that the association would automatically be created with the correct foreign key

Actual behavior

As shown by the sql statements in the debug log, it was created using default values.
I thought setting a sequence, but a sequence generally won't be necessary for this factory,
and I wouldn't want the sequence to collide when I need a factory to create a model with
a specific id.

It is possible to work-around this by adding an after(:build) callback, in which case, the
association doesn't need to be defined inside the factory.

System configuration

factory_bot version: 6.2.1
rails version: 6.0.4.8
ruby version: 3.1.1p18

With the factory you presented, this call:

vendor = FactoryBot.create(:vendor, vendor_no: 5)

ultimately runs something like:

vendor = Vendor.new
vendor.vendor_no = 5
vendor_item = VendorItem.new
vendor.vendor_item = vendor_item
vendor.save!

The assignment of a new VendorItem with no primary key will wipe out the foreign key on the vendor.

Solving with an after(:build) callback seems reasonable. You might also be able to use the association method inside the block:

  factory :vendor do
    vendor_item { association :vendor_item, vendor_no: vendor_no }
  end

Which would make it behave more like:

vendor = Vendor.new
vendor.vendor_no = 5
vendor_item = VendorItem.new
vendor_item.vendor_no = vendor.vendor_no
vendor.vendor_item = vendor_item
vendor.save!

Thank you for your response! The FactoryBot.create explanation and the association method example were very helpful. I looked at the docs for belongs_to to understand why the assignment of a new VendorItem with no primary key would wipe out the foreign key on the vendor.

belongs_to
association=(associate)
Assigns the associate object, extracts the primary key, and sets it as the foreign key...
Post#author=(author) (similar to post.author_id = author.id)

has_one
association=(associate)
Assigns the associate object, extracts the primary key, sets it as the foreign key, and saves the associate object...
Account#beneficiary=(beneficiary) (similar to beneficiary.account_id = account.id; beneficiary.save)

I realized that changing the vendor_item association from belongs_to to has_one would fix the test. I changed my Vendor class from

class Vendor < ActiveRecord::Base
  self.primary_key = :vendor_no
  belongs_to :vendor_item, foreign_key: :vendor_no, primary_key: :vendor_no
end

to

class Vendor < ActiveRecord::Base
  self.primary_key = :vendor_no
  has_one :vendor_item, foreign_key: :vendor_no, primary_key: :vendor_no, required: true
end