TIL: ruby factory bot and models association : `after(:build)`
Jean Roger Nigoumi Guiala
Posted on January 25, 2023
so I had an issue today , I have a model (we will call it StatusMain) that embeds another model(we will call it HistoricalStatus), an example below
# ruby '3'
class StatusMain
include Mongoid::Document
include Mongoid::Timestamps
include AASM
field :status, type: String, default: 'initiated'
embeds_many :historical_statuses
scope :initiated, -> { where(status: 'initiated') }
scope :rejected, -> { where(status: 'rejected') }
aasm column: :status do
after_all_transitions :create_history
state :initiated, initial: true
state :rejected
event :reject do
transitions from: :initiated, to: :rejected
end
end
def to_p
Protos:: StatusInitial.new(
id: id.as_json['$oid'],
status: status,
historical_statuses: historical_statuses.collect(&:to_p),
)
end
private
def create_history
historical_statuses.create(
status_main_id: id,
status: aasm.to_state,
created_at: Time.now,
updated_at: Time.now,
)
end
end
and the historical_status model looks something like this
# ruby '3'
class HistoricalStatus
include Mongoid::Document
include Mongoid::Timestamps
field :status_main_id, type: BSON::ObjectId
field :status, type: String
validates :status_main, presence: true
validates :status, presence: true
embedded_in :status_main
def to_p
Protos::HistoricalStatus.new(
id: id.as_json['$oid'],
status_main_id: status_main_id.as_json['$oid'],
status: status,
created_at: created_at.iso8601,
updated_at: updated_at.iso8601,
)
end
end
so as you can see, the goal is to have the historical_status store every status transitions, and be embedded in my StatusMain
so as I was working on my spec , I designed my factories like this
- status_main factory
# ruby '3'
FactoryBot.define do
factory :status_main do
status { 'initiated' }
historical_statuses do
[
create(:historical_status, status_main: instance),
]
end
end
end
*historical_status factory
# ruby '3'
FactoryBot.define do
factory :historical_status do
status_main
status_main_id { status_main.id }
status { 'initiated' }
trait :initiated do
status { 'initiated' }
end
trait :rejected do
status { 'rejected' }
end
end
end
So as you can see , in my factories, historical_status receives the status_main, that way , in status_main factory, I can create the historical_status with the correct status_main id.
The problem
so the issues I had is that , my status_main_id was always nil in my historical_status , that caused my test to fail because as you can guess .as_json['$oid'],
does not exist on nil object.
Tried multiple things and I was surprised that my status_main_id is nil... How can it be nil when I'm passing it and doing it correctly.
Turns out my status_main object that I'm passing to historical_status was nil... now it's interesting, I continued investigating and found out , that my status_main
object was set AFTER my historical_status
was created, thus status_main_id
was nil in historical_status.
The solution
let me first paste the updated factory and then I will explain.
# ruby '3'
FactoryBot.define do
factory :historical_status do
status_main
status { 'initiated' }
after(:build) do |historical_status, evaluator|
historical_status.status_main_id = evaluator.status_main.id
end
trait :initiated do
status { 'initiated' }
end
trait :rejected do
status { 'rejected' }
end
end
end
so as you can notice , we now have an after(:build)
In the new factory, I'm using the after(:build) callback to set the status_main_id after the historical_status object has been built, and before it's saved, this way you can use the evaluator to get the id of the status_main
and set it to the status_main_id
field. This is important because the factory is trying to use the id of the status_main
object that is associated with the historical_status and if the id is not set yet it will raise an error.
The evaluator is an instance of FactoryBot::Evaluator that is passed to the block, which is used to access the attributes of the factory and the objects that are being built/created by the factory.
By setting the status_main_id
in the after(:build) callback, you ensure that the status_main_id
field is set before the historical_status object is saved, and the id will be the same as the original status_main
id
field as wanted.
Conclusion
So here is what I learned today , feel free to comment a better way to achieve that if you have some, or if you can explain in a different way what is happening.
Posted on January 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.