People often forget that FactoryBot1 is just using Ruby to instantiate objects. Most of the time, yes, you’ll use FactoryBot in conjunction with your favorite ORM to instantiate objects for testing Rails apps.
Want a simplistic stub?
FactoryBot.define do
factory :tiny_stub, class: OpenStruct
end
# >> user = FactoryBot.build(:tiny_stub, admin?: true, name: 'John Doe')
# => #<OpenStruct admin?=true, name="John Doe">
# >> user.admin?
# => true
# >> user.name
# => "John Doe"
Let’s go a bit crazy: what about creating a factory that generates URLs? Writing out valid string URLs is a pain and copy/paste seems both problematic and error-prone. If I want to change the protocol to https:// or switch a subdomain, it gets rougher. Throw in ports and paths and you’re itching for a headache.
So, a factory for generating URLs. First thing’s first: what are the properties we need?
factory :url do
protocol { 'http://' }
host { 'www.example.com' }
port { '80' }
end
That’s a start.
# >> FactoryBot.create(:url)
# NameError: uninitialized constant Url
Ah, forgot that we want this to be an instance of string.
factory :url, class: String do
protocol { 'http://' }
host { 'www.example.com' }
port { '80' }
end
# >> FactoryBot.create(:url)
# NoMethodError: undefined method `protocol=' for "":String
Makes sense; let’s build this string manually.
factory :url, class: String do
ignore do
protocol { 'http://' }
host { 'www.example.com' }
port { '80' }
end
initialize_with { new("#{protocol}#{host}:#{port}") }
end
# >> FactoryBot.create(:url)
# NoMethodError: undefined method `save!' for "http://www.example.com:80":String
Let’s tell the factory to skip creation when we call create
:
factory :url, class: String do
skip_create
ignore do
protocol { 'http://' }
host { 'www.example.com' }
port { '80' }
end
initialize_with { new("#{protocol}#{host}:#{port}") }
end
# >> FactoryBot.create(:url)
# => "http://www.example.com:80"
This is looking better.
What don’t I like about this right now? I’d have to specify protocol manually if I want to make it secure:
# >> FactoryBot.create(:url, protocol: 'https://', port: nil)
# => "https://www.example.com:"
Yuck; it’s all sorts of broken. Let’s use a trait to make this more managable.
factory :url, class: String do
skip_create
ignore do
protocol { 'http://' }
host { 'www.example.com' }
port { '80' }
end
trait :secure do
protocol { 'https://' }
port { nil }
end
initialize_with { new("#{protocol}#{[host, port].compact.join(':')}") }
end
# >> FactoryBot.create(:url, :secure)
# => "https://www.example.com"
Up next: subdomain and domain name!
factory :url, class: String do
skip_create
ignore do
protocol { 'http://' }
subdomain { 'www' }
domain_name { 'example.com' }
host { [subdomain, domain_name].compact.join('.') }
port { '80' }
end
trait :secure do
protocol { 'https://' }
port { nil }
end
initialize_with { new("#{protocol}#{[host, port].compact.join(':')}") }
end
# >> FactoryBot.create(:url, subdomain: 'blog')
# => "http://blog.example.com:80"
Finally, path:
factory :url, class: String do
skip_create
ignore do
protocol { 'http://' }
subdomain { 'www' }
domain_name { 'example.com' }
host { [subdomain, domain_name].compact.join('.') }
port { '80' }
path { '/' }
end
trait :secure do
protocol { 'https://' }
port { nil }
end
initialize_with { new("#{protocol}#{[host, port].compact.join(':')}#{path}") }
end
# >> FactoryBot.create(:url, path: '/about/us')
# => "http://www.example.com:80/about/us"
So, what’s this give us?
# >> FactoryBot.create(:url)
# => "http://www.example.com:80/"
# >> FactoryBot.create(:url, :secure)
# => "https://www.example.com/"
# >> FactoryBot.create(:url, :secure, subdomain: 'blog', path:
# >> '/12345/great-post-title')
# => "https://blog.example.com/12345/great-post-title"
# >> FactoryBot.create(:url, domain_name: 'example.co.uk', port: '1234')
# => "http://www.example.co.uk:1234/"
For more fun(ctionality), we can add child factories:
factory :url, class: String do
# ...
factory :twitter do
secure
subdomain { nil }
domain_name { 'twitter.com' }
end
factory :facebook do
secure
subdomain { nil }
domain_name { 'facebook.com' }
end
factory :google_analytics do
domain_name { 'google.com' }
port { nil }
path { '/analytics' }
end
end
# >> FactoryBot.create(:twitter)
# => "https://twitter.com/"
# >> FactoryBot.create(:facebook)
# => "https://facebook.com/"
# >> FactoryBot.create(:google_analytics)
# => "http://www.google.com/analytics"
FactoryBot is really powerful. With the ability to specify the factory’s
class, traits, initialize_with
, skip_create
, and dynamic attributes, we
can build a flexible factory that can be used whenever you need URLs - all
without having to build strings by hand.
What custom factories have you built?
Project name history can be found here.
-
Looking for FactoryGirl? The library was renamed in 2017. ↩