Using factory_bot without Ruby on Rails
Preface about factory_bot
In my work on Ruby on Rails projects, I’m a fan of using Thoughtbot’s factory_bot as a replacement for the built-in concept of fixtures. If you’re unfamiliar, the premise is pretty simple. Rails’ fixtures are nice, but are really limited, and tend to hamstring developers into writing unmaintainable tests. A fixture looks something like this:
class Song < ApplicationRecord
# has ActiveRecord attributes title, artist, year
validates :title, :artist, :year, presence: true
end
a_cool_song:
title: All My Friends
artist: LCD Soundsystem
year: 2007
It’s easy to see that in a complex system, it would be necessary to either create a bunch of different fixtures or modify them in each test scenario. Doable for smaller projects, but it quickly becomes unweildy. Factories from factory_bot are like supercharged fixtures. Instead of defining a bunch of fixtures, we define a single factory for our Song
model:
FactoryBot.define do
factory :song do
title { 'All My Friends' }
artist { 'LCD Soundsystem' }
year { 2007 }
end
end
In our test scenarios, creating an instance of Song
, even with potentially different attributes, is super easy:
pry(main)> FactoryBot.create(:song)
=> #<Song:0x00007ff42b84b008 @artist="LCD Soundsystem", @title="All My Friends", @year=2007>
pry(main)> FactoryBot.create(:song, title: 'call the police', year: 2017)
=> #<Song:0x00007ff42a321510 @artist="LCD Soundsystem", @title="call the police", @year=2017>
pry(main)>
This is only scratching the surface of factory_bot’s capabilities. For more, check out its getting started document.
Adapting factory_bot to work with plain Ruby
factory_bot was written and continues to be maintained with a huge focus on supporting Ruby on Rails - which is fine. It works wonderfully there. With just a couple small tweaks we can make it work nicely with POROs (Plain Old Ruby Objects). In some sense, it already does work, but there are a couple things to do to make it easier to work with.
Fixing a broken call to ActiveRecord
Trying to use a factory out of the box might look like this:
class Song
attr_accessor :title, :artist, :year
end
pry(main)> FactoryBot.create(:song)
NoMethodError: undefined method `save!' for #<Song:0x00007f915fae6aa8>
factory_bot is trying to call ActiveRecord::Persistence#save!
. We’re not using ActiveRecord, so we need to skip this. To do so, we can add skip_create
to our factory definition.
FactoryBot.define do
factory :song do
skip_create
title { 'All My Friends' }
artist { 'LCD Soundsystem' }
year { 2007 }
end
end
pry(main)> FactoryBot.create(:song)
=> #<Song:0x00007ff42b84b008 @artist="LCD Soundsystem", @title="All My Friends", @year=2007>
Fixing object initialization
factory_bot’s actions are the same as they are in a Rails context, but it’s important to understand exactly what it does to create the object. The first thing that factory_bot does is actually instantiate the object, and run its initialize
method, if it exists. This is sometimes a problem - factory_bot won’t pass any parameters into the call to Song.new
, even if it’s required. Consider a Song
class like this:
class Song
attr_accessor :title, :artist, :year
def initialize(something)
@title = "A Placeholder Title: #{something}"
end
end
What happens if we use our factory as-is?
pry(main)> FactoryBot.create(:song)
ArgumentError: wrong number of arguments (given 0, expected 1)
from test.rb:7:in `initialize'
Clearly, factory_bot is just calling Song.new
and calling it a day. We can fix this with initialize_with
, though:
FactoryBot.define do
factory :song do
skip_create
title { 'All My Friends' }
artist { 'LCD Soundsystem' }
year { 2007 }
initialize_with { new('foo') }
end
end
pry(main)> FactoryBot.create(:song)
=> #<Song:0x00007ff42b84b008 @artist="LCD Soundsystem", @title="All My Friends", @year=2007>
factory_bot is now constructing the object in the way that we want, and everything works. You might notice that despite the initialize
method setting the @title
instance variable to some value, the value of 'All My Friends'
takes prescendence. This is because the initialize
method is evaluated first, and then factory_bot does simple assignment to the instance variables (@title = 'All My Friends'
and so on…). This is why we need to include attr_accessor
or attr_writer
for all of the instance variables we define in our factory.
Associations with other objects
You can still use factory_bot’s association
method to call other factories, the same as in Rails. As long as there as an instance variable with the same name, it should work just fine.
class Album
attr_accessor :title, :artist, :year
end
class Song
attr_accessor :title, :album
end
FactoryBot.define do
factory :album do
skip_create
title { 'Sound of Silver' }
artist { 'LCD Soundsystem' }
year { 2007 }
end
factory :song do
skip_create
title { 'All My Friends' }
album { association(:album) }
end
end
Create the association automatically because we didn’t pass it into the create
call:
pry(main)> s = FactoryBot.create(:song)
=> #<Song:0x00007f9f612eec28
@album=#<Album:0x00007f9f639f38f0 @artist="LCD Soundsystem", @title="Sound of Silver", @year=2007>,
@title="All My Friends">
pry(main)> s.album
=> #<Album:0x00007f9f639f38f0 @artist="LCD Soundsystem", @title="Sound of Silver", @year=2007>
Set the association to an existing object by passing it into the create
call as a named parameter:
pry(main)> a = FactoryBot.create(:album)
=> #<Album:0x00007f9f639a0df8 @artist="LCD Soundsystem", @title="Sound of Silver", @year=2007>
pry(main)> s = FactoryBot.create(:song, album: a)
=> #<Song:0x00007f9f61366f20
@album=#<Album:0x00007f9f639a0df8 @artist="LCD Soundsystem", @title="Sound of Silver", @year=2007>,
@title="All My Friends">
Note that the memory address in the Album
object identifier is 0x00007f9f639a0df8
in both cases. This means we know for a fact that the same instance of Album
we created first was assigned to the @album
instance variable on the Song
instance, as we wanted.
That’s all for now, happy hacking!