DEV Community

Cover image for TIL: ruby factory bot and models association : `after(:build)`
Jean Roger Nigoumi Guiala
Jean Roger Nigoumi Guiala

Posted on

TIL: ruby factory bot and models association : `after(:build)`

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

*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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)