Post hero cover

FactoryGirl Beyond the Database

I’ve been experimenting with factory_girl lately, particularly to deal with test data that’s not necessarily tied to the database. Did you know you can use it to instantiate any Ruby object, and not only ActiveRecord models?

This was already covered by the gem’s creators a few years ago. But doing a quick search on WhoUsesThisGem, and skimming through the factories of some of the major projects tells that either this is not a well-known feature, or maybe it’s just not that useful. My money goes on the former.

Why?

Working with external services and APIs I often find the need to mock their responses on my tests. There is one particular case where I’m using the Slack API to authenticate teams and interact with them.

The authentication step uses Slack’s OAuth flow. To test it, I need to be able to mock the OAuth response, which usually consists of a hash containing some information about the team that just signed in. Here is a sample of it:

{
  "uid" => "AN ID",
  "info" => {
    "name" => "Miguel Palhas",
    "email" => "miguel@subvisual.co",
    ...
  },
  "extra" => {
    ...
  }
    "credentials" => {
    "token" => "A SECRET TOKEN",
    "expires" => false
  }
}

The common way to mock this response is described here, but in short, it consists of the following:

OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new({
  "uid" => "AN ID",
  "info" => {
    "name" => "Miguel Palhas",
    "email" => "miguel@subvisual.co",
    ...
  },
  ...
})

This works for most applications, but I want to easily create multiple unique hashes (e.g.: I should be able to test that two teams can authenticate independently, and for that I need two different OAuth hashes).

How?

It would be great to use a factory to generate the same kind of object. Let’s see how that can be achieved:

## spec/factories/omniauth.rb

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

I’m using the class argument to configure this factory to instantiate OmniAuth hashes. Any Ruby class can be used here. skip_create is a flag that instructs the factory not to call save! on the new instance, which is the default behaviour, and is what persists the records when we’re dealing with ActiveRecord objects. In this case, I only want to create an object in memory, so I disable that behaviour.

  transient do
    sequence(:uid)
    provider "slack"
    token "MyToken"
  end

I define a few attributes for my factory. I don’t want to have these attributes directly assigned to my instance (that wouldn’t even work), so I wrap them in an transient block, which makes them be defined and evaluated in my factory, but only for internal use, as we’ll see below.

  initialize_with do
    OmniAuth::AuthHash.new({
      provider: provider,
      uid: uid,
      credentials: {
        token: token
      }
    })
  end
end

The initialize_with block lets me change how the instance is created. The default is to call OmniAuth::Hash.new because that’s the class I specified, but I want to assign some attributes myself, using the schema that I expect OAuth to give me.

With all of this in place, I can create as many hashes as I want with the usual calls to factory girl:

hash1 = FactoryGirl.create(:slack_auth_hash)
hash1.uid
##=> 1

hash2 = FactoryGirl.create(:slack_auth_hash)
hash2.uid
##=> 2

Because I used a sequence for the uid attribute, I easily get a different one for each hash that I create. This is necessary for my tests, as it allows me to create multiple teams in the same spec, and each of them will be unique.

I can also use traits to provide variations of this hash. For example, to test how my app deals with invalid OAuth hashes:

trait :invalid do
  initialize_with do
    FactoryGirl.
      build(:slack_auth_token).
      except(:credentials)
  end
end

My app requires the token to exist in the OAuth response. So I can define an invalid hash as being the same as the original with, but without the credentials section, which contains the token. I can then use this factory to check that my app detects the error, and does not authenticate the user.

Here’s the full factory we built:

## spec/factories/omniauth.rb

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

  transient do
    sequence(:uid)
    provider "slack"
    token "MyToken"
  end

  initialize_with do
    OmniAuth::AuthHash.new({
      provider: provider,
      uid: uid,
      credentials: {
        token: token
      }
    })
  end

  trait :invalid do
    initialize_with do
      FactoryGirl.
        build(:slack_auth_token).
        except(:credentials)
    end
  end
end

There are many more use cases for this feature, and some great ones were already covered in thoughbot’s post, so be sure to check that out if you’re interested.

Miguel Palhas

About Miguel Palhas

All-round developer with a background in High Performance Computing and a passion for the Open Source world. Still dreams about becoming a real game developer someday.
Like what you've read so far? Let's work together.