Factory Girl without Active Record

https://upload.wikimedia.org/wikipedia/commons/0/0e/Rhodes-mfg-co.jpg

Factory Girl has been around for more than five years now and has become the standard for building and saving valid model data for your test suite. Out of the box, Factory Girl plays nicely with the major ORMs: Active Record, Mongoid, DataMapper and MongoMapper. But what about those pesky models that fall outside of your ORM? Fear not… Factory Girl's got you covered there too!

Non-ORM Models

Developers often define their models around their database tables but as your application grows and becomes more interconnected, you will begin to pull data from sources outside your own database. Make no mistake: the Ruby objects that encapsulate this external data are still models, even without your ORM of choice.

I'll share examples of two external models from an application I've been working on.

Facebook Auth

The application uses OmniAuth to register and authenticate users via Facebook. OmniAuth documentation describes the structure of the Auth Hash returned from Facebook. This is our external data. OmniAuth wraps this hash in a class called OmniAuth::AuthHash. That is our model.

When a user authenticates via Facebook, the application either finds an existing user or creates a new one from the auth hash that Facebook returned.

class User < ActiveRecord::Base
  def self.from_omniauth(facebook_auth)
    raw_info = facebook_auth.extra.raw_info

    find_or_create_by!(facebook_id: raw_info.id) do |user|
      user.first_name = raw_info.first_name
      user.last_name = raw_info.last_name
      user.email = raw_info.email
      user.gender = raw_info.gender
    end
  end
end

In testing this method, it would be really helpful to have a realistic auth hash to work with. Enter: Factory Girl.

FactoryGirl.define do
  factory :facebook_auth, class: OmniAuth::AuthHash do
    skip_create

    ignore do
      id { SecureRandom.random_number(1_000_000_000).to_s }
      name { "#{first_name} #{last_name}" }
      first_name "Joe"
      last_name "Bloggs"
      link { "http://www.facebook.com/#{username}" }
      username "jbloggs"
      location_id "123456789"
      location_name "Palo Alto, California"
      gender "male"
      email "joe@bloggs.com"
      timezone(-8)
      locale "en_US"
      verified true
      updated_time { SecureRandom.random_number(1.month).seconds.ago }
      token { SecureRandom.urlsafe_base64(100).delete("-_").first(100) }
      expires_at { SecureRandom.random_number(1.month).seconds.from_now }
    end

    provider "facebook"
    uid { id }

    info do
      {
        nickname: username,
        email: email,
        name: name,
        first_name: first_name,
        last_name: last_name,
        image: "http://graph.facebook.com/#{id}/picture?type=square",
        urls: { Facebook: link },
        location: location_name,
        verified: verified
      }
    end

    credentials do
      {
        token: token,
        expires_at: expires_at.to_i,
        expires: true
      }
    end

    extra do
      {
        raw_info: {
          id: uid,
          name: name,
          first_name: first_name,
          last_name: last_name,
          link: link,
          username: username,
          location: { id: location_id, name: location_name },
          gender: gender,
          email: email,
          timezone: timezone,
          locale: locale,
          verified: verified,
          updated_time: updated_time.strftime("%FT%T%z")
        }
      }
    end
  end
end

What makes this factory special is the skip_create method call and the ignore block.

The skip_create method does just what it says. Rather than using Factory Girl's default behavior of trying to save the instance to the database, persistence is skipped.

The ignore block defines a list of attributes that won't be passed into the new instance but can be used elsewhere in the factory. Ignored attributes are perfect for deeply nested structures like the auth hash factory above. I can specify a first_name and it will appear throughout the new auth hash.

facebook_auth = FactoryGirl.create(:facebook_auth, first_name: "Steve")

facebook_auth.info.first_name           # => "Steve"
facebook_auth.info.name                 # => "Steve Bloggs"
facebook_auth.extra.raw_info.first_name # => "Steve"
facebook_auth.extra.raw_info.name       # => "Steve Bloggs"

It's incredibly handy to have a completely valid Facebook auth hash available from anywhere in your test suite. Plus, this factory should not need to change often, if at all.

Balanced Customer

Balanced is a payment processing company designed for marketplaces. They can process payments from customers and payouts to sellers. They have a robust API with its own Ruby wrapper. The API is our external data and the wrapper's resource classes are our models.

Defining factories for these external resources is done similarly to the Facebook auth hash above and takes advantage of a couple more techniques: the initialize_with method and "traits."

FactoryGirl.define do
  trait :balanced_resource do
    skip_create

    initialize_with do
      new.class.construct_from_response(attributes)
    end

    ignore do
      balanced_marketplace_uri { ENV["BALANCED_MARKETPLACE_URI"] }
    end

    id { SecureRandom.urlsafe_base64(24).delete("-_").first(24) }
  end

  factory :balanced_card, class: Balanced::Card do
    balanced_resource

    account nil
    brand "MasterCard"
    card_type "mastercard"
    country_code nil
    created_at { Time.current.xmlschema(6) }
    customer nil
    expiration_month 12
    expiration_year 2020
    hash { SecureRandom.hex(32) }
    is_valid true
    is_verified true
    last_four "5100"
    meta({})
    name nil
    postal_code nil
    postal_code_check "unknown"
    security_code_check "passed"
    street_address nil
    uri { "#{balanced_marketplace_uri}/cards/#{id}" }
  end

  factory :balanced_bank_account, class: Balanced::BankAccount do
    balanced_resource

    account_number "xxxxxx0001"
    bank_name "BANK OF AMERICA, N.A."
    can_debit false
    created_at { Time.current.xmlschema(6) }
    credits_uri { "#{uri}/credits" }
    customer nil
    debits_uri { "#{uri}/debits" }
    fingerprint { SecureRandom.hex(32) }
    meta({})
    name "Johann Bernoulli"
    routing_number "121000358"
    type "checking"
    uri { "/v1/bank_accounts/#{id}" }
    verification_uri nil
    verifications_uri { "#{uri}/verifications" }
  end
end

By default, Factory Girl passes its attributes hash to the new method. This works in most cases but unfortunately, not for our Balanced resources. They instead use the class method construct_from_response. Currently, getting access to a class method within the initialize_with block is awkward but can be done by calling the method on new.class.

You can see that we also registered a balanced_resource trait. A trait is a set of attributes and behaviors that can be reused across other factories. The balanced_resource trait sets up initialization, skips create and gives us some default attributes. The balanced_card and balanced_bank_account factories can then call that trait and inherit all of the Balanced-specific behavior.

One more thing…

Using Factory Girl for API-backed models like the Balanced models above allows you to go further and stub how the models find their instances.

Even though we skip_create in our Balanced factories, the after(:create) callback can still be fired. If we tap into that callback, we can stub the Balanced library's find method to return the instance we just "created."

factory :balanced_card, class: Balanced::Card do
  balanced_resource

  account nil
  # ...
  uri { "#{balanced_marketplace_uri}/cards/#{id}" }

  after(:create) do |card|
    card.class.stub(:find).with(uri).and_return(card)
  end
end

This approach can clean up your test suite and allow you to more easily write tests that don't require a connection to your external services.


Check out our latest product, Dead Man’s Snitch for monitoring cron, heroku scheduler or any periodic task.

Dead Man's Snitch

Photo of Steve Richert

Steve is a Senior Developer working with Ruby/Rails and JavaScript. He’s an active open source contributor and the lead developer for Interactor. Steve is also involved in documenting and improving Collective Idea’s software development practices.

Comments

Add a Comment

Hmm...that didn't work.

Something went wrong while adding your comment. If you don't mind, please try submitting it again.

Comment Added!

Your comment has been added to this post. Please refresh this page to view it.

Optional. If added, we will display a link to the website in your comment.
Optional. Never shared or displayed in your comment.
  1. November 07, 2013 at 23:23 PM

    This is really a great way to use FactoryGirl for non-ActiveRecord models. Thanks Steve!

  2. November 10, 2013 at 22:36 PM

    Excellent, just what I was looking for! Thank you very much!

  3. pupuwayne@gmail.com
    Wayne
    January 07, 2014 at 13:29 PM

    hello, i have some trouble about
    while i create a instance by FactoryGirl.create and try to access the attribute define in ignore block, i got
    NoMethodError: undefined method `info’
    What’s wrong?

    i just do something like this:
    FactoryGirl.define do
    factory :facebook_auth, class: MyAPI::Cls do
    skip_create
    ignore do
    id { SecureRandom.random_number(1_000_000_000).to_s }
    end
    end
    end

  4. steve@collectiveidea.com
    Steve Richert
    January 07, 2014 at 15:21 PM

    Wayne, ignored attributes are only available from within the factory definition. They are not actually set on the resulting instance. I hope that helps!

  5. sean@iheartsquares.com
    Sean Wolfe
    February 22, 2014 at 1:10 AM

    This still doesn’t work if you want to use the stub strategy. It still attempts to pretend to save your model, and set an :id value. This is useless if your custom model doesn’t have an :id field.

  6. ferr
    Tom Tuddenham
    October 24, 2014 at 3:59 AM

    Excellent. Hugely helpful article - thank you

  7. msarioya345@gmail.com
    mani kumat saroya
    April 30, 2015 at 15:42 PM

    I love u

  8. steve@collectiveidea.com
    Steve Richert
    April 30, 2015 at 15:57 PM

    I love u 2

  9. abdutahir
    December 08, 2016 at 3:36 AM

    It is what I am looking for. Thanks