Arranging Complex Factories (for fun and profit)
FactoryGirl
the oddly named testing tool for database models is something we use a lot at Wizard Development. If you're unfamiliar with how it works, I strongly suggest you check it out. Unlike fixtures which loads a bunch of data into your database, factories will create the objects you need with the data preloaded. The difference allows you to only get what you need, allows skipping using the database all together (for much faster and more isolated tests) and a few other great things.
Individual models are fairly straightforward to test. I'll be using examples with rspec
and ActiveRecord
.
# Model
class User < ActiveRecord::Base
scope :admins, -> { where(admins: true) }
def admin!
update!(admin: true)
end
end
# Factory
FactoryGirl.define do
factory :user do
sequence(:name) { |n| "#{Faker::Name.name} #{n}" }
sequence(:email) { |n| "#{n}#{Faker::Internet.email}" }
admin false
trait :as_admin do
admin true
end
end
end
# Test
require 'rails_helper'
describe User do
let(:user) { build_stubbed(:user) }
let(:admin) { build_stubbed(:user, :as_admin) }
describe '#admin!' do
it 'makes a user an admin' do
user.admin!
expect(user.admin?).to eq(true)
end
it 'keeps admins in power' do
admin.admin!
expect(admin.admin?).to eq(true)
end
end
describe '.admins' do
it 'returns only the admins' do
admin = create(:admin)
create(:user)
expect(User.admins).to contain_exactly(admin)
end
end
end
For the model's instance functions we used FactoryGirl.build_stubbed
a method that creates a model that pretends it's saved to the database. All validations, and database methods will pretend to work as expected and we'll be sure to never actually talk to the database, which is significantly faster.
For the scope we have to talk to the database so we create both models and ensure the result only has the admin
object. Since only those two objects are needed, that's all we make.
As your app grows you'll start needing to test service objects that work with several models at once, and your models themselves will get more complected requiring each other to be in specific states to be valid. It's going to get difficult to have a single factory properly setup the environment for testing. (If you find it impossible to use factories you need to have a long hard look at your design because it wont ever get easier on it's own.)
A common situation is when you want "Multitenancy" where your app needs to support users having their own objects. This is very straightforward to support with factories, at first.
class User < ActiveRecord::Base
has_one :photo
end
class Photo < ActiveRecord::Base
validates :user, presence: true
end
FactoryGirl.define do
factory :user do
sequence(:name) { |n| "#{Faker::Name.name} #{n}" }
trait :with_photo do
photo
end
end
factory :photo do
title "My cat Kris"
end
end
# to build a stubbed user with a stubbed photo
FactoryGirl.build_stubbed(:user, :with_photo)
Now you'll probably want to support a user with many photos. FactoryGirl suggessts using the after and before callbacks for creating the associations. Lets try it
class User < ActiveRecord::Base
has_many :photos
end
class Photo < ActiveRecord::Base
end
FactoryGirl.define do
factory :user do
sequence(:name) { |n| "#{Faker::Name.name} #{n}" }
trait :with_photos do
transient do
photo_count 2
end
after(:create) do |user, evaluator|
create_list(:photo, evaluator.photo_count, user: user)
end
end
end
factory :photo do
title "My cat Kris"
end
end
# to create a user with 2 photos
FactoryGirl.create(:user, :with_photos)
# When we build_stubbed or build or any of the other methods, we no longer have any photos!
FactoryGirl.build(:user, :with_photo) # no photos!
FactoryGirl.build_stubbed(:user, :with_photos) # no photos!
Since this approach only works with specific methods, you'll either need to write callbacks for each method or do some magic. I'll rewrite the factory with some magic.
FactoryGirl.define do
factory :user do
sequence(:name) { |n| "#{Faker::Name.name} #{n}" }
trait :with_photos do
transient do
photo_count 2
end
photos do |t|
photo_count.times.map {
t.association(:photo, user: t.instance_variable_get(:@instance))
}
end
end
end
factory :photo do
title "My cat Kris"
end
end
# Now however we want our test data we'll get what we expect!
FactoryGirl.create(:user, :with_photos)
FactoryGirl.build(:user, :with_photo)
FactoryGirl.build_stubbed(:user, :with_photos)
The t
that's passed into the block on photos
, I think this is called an evaluator
internal to FactoryGirl, but I'm not positive. Names are hard. We're able to use the t.association
to mimic however we called the parent factory. When we are building a factory it uses build()
when we're creating it uses create()
. Yay!
I know t.instance_variable_get(:@instance)
looks very strange but there doesn't seem to be another way to get a reference to the parent object to give to the child object. Not all children need their parents, but when they do you need to provide them.
We should also note that we're using a transient attribute to allow us to customize how many photos get created.
# if we want a ton of photos
FactoryGirl.create(:user, :with_photos, photo_count: 400)
Lets go for an even more complex example.
class User < ActiveRecord::Base
has_many :photos
has_one :album
end
class Album < ActiveRecord::Base
has_many :photos
validates :user, presence: true
end
class Photo < ActiveRecord::Base
validates :user, presence: true
end
Photos still belong to users but can now also belong to an album that belongs to a user. Lets also ensure there's always a user for these objects.
A factory setup could be
FactoryGirl.define do
factory :user do
sequence(:name) { |n| "#{Faker::Name.name} #{n}" }
trait :with_album do
album
end
end
factory :photo do
title "My cat Kris"
user
end
factory :album do
user
title "Kitties"
transient { photo_count 2 }
photos do |t|
photo_count.times.map { t.association(:photo) }
end
end
end
Lets try this out
user = FactoryGirl.create(:user, :with_album)
user.album.photos.count # 2
user.photos.count # 0 !?!?!?!
User.count # 4 !!!!!
We have a user with an album of other users photos! That's not what we wanted.
The photo factory was creating it's own users for it's photos since we didn't specify who should own them. Additionally the album factory created a user and then got assigned to the one we created. Lets try again.
FactoryGirl.define do
factory :user do
sequence(:name) { |n| "#{Faker::Name.name} #{n}" }
trait :with_album do
album { t.association(:album, :with_photos, user: t.instance_variable_get(:@instance))
end
end
factory :photo do
title "My cat Kris"
user
end
factory :album do
user
title "Kitties"
trait :with_photos do
transient { photo_count 2 }
photos do |t|
photo_count.times.map { t.association(:photo, user: user) }
end
end
end
end
The user now gives itself to the album, the album now gives it's user to the photos and we always get what we expect. We can create any factory and get a user who owns the photos that were generated and never get more users than we expect.
I think this is too complicated. I'm convinced there are easier ways to do the advanced examples in this blog post. When I find them I'll happily update this post and a bunch of my factory code. In the meantime I'll live with slightly complicated factories and enjoy easier testing.
Let me leave you with a with a small spec we include with most projects. It ensures that every factory and trait is valid and can be stubbed. And helps you keep all factories usable with expected results. FactoryGirl.lint
has some unexpected creation of models and doesn't cleanup after itself. If you're using Foreign Key Constraints you'll get an added bonus of errors when you accidently create models related to stubbed models.
require 'rails_helper'
FactoryGirl.factories.map(&:name).each do |factory_name|
describe "#{factory_name} factory" do
it 'builds valid' do
model = FactoryGirl.build(factory_name)
expect(model).to be_valid if model.respond_to?(:valid?)
end
it 'builds stubbed' do
model = FactoryGirl.build_stubbed(factory_name)
expect(model).to be_valid if model.respond_to?(:valid?)
end
end
end
Powered by ⚡️ and 🤖.