---
title: Mind-Bending Factories
teaser:
tags: web,ruby,testing,open source
author: Josh Clayton
published_on: 2012-07-31
---

People often forget that FactoryBot[^1] 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?

[^1]: Looking for FactoryGirl? The library was renamed in 2017.
[Project name history can be found here.](https://github.com/thoughtbot/factory_bot/blob/master/NAME.md)
