Michael Anhari

Rails test objects up and running with FactoryBot

White robot smiling up at the camera

Over the next few days, I thought we could cover some essential gems that I like to use when testing Rails applications with either MiniTest or RSpec. I thought the best place to start this discussion would be talking about one can generate test data using a gem from thoughtbot that you'll likely find in most code bases.

Creating test data with FactoryBot (beep boop beep!)

factory_bot_rails might be the first gem I add to every Rails project. The factory pattern is an object-oriented approach for creating an interface for spinning up various objects, and that's exactly what factory_bot_rails does by introducing a friendly DSL.

Simple example factory

Let's say we have an Article model in our Rails CMS application. In such a codebase, we might build a factory like this to create data in our tests:

FactoryBot.define do
  factory :article do
    # default attribute values defined here
    title { "Best article ever" }
    description  { "No really this is the best article I've ever read!" }
    published { false }
  end
end

Let's say we wanted to write a scope in our Article model to return unpublished articles (or drafts).

class Article < ApplicationRecord
  scope :drafts, -> { where(published: false) }
end

A test for this using factory_bot_rails and MiniTest might look something like this:

 test '.drafts should only return articles that are not published' do
    draft = create(:article, published: false)
    published_article = create(:article, published: true)

    results = Article.drafts

    assert_includes(results, draft)
    assert_not_includes(results, published_article)
  end

Extending our factory using traits

Right now our article factory only creates one type of article by default. Let's add some traits to show how flexible FactoryBot can be.

FactoryBot.define do
  factory :article do
    # default attribute values defined here
    title { "Best article ever" }
    description  { "No really this is the best article I've ever read!" }
    published { false }

    trait :published do
      published { true }
    end

    trait :draft do
      published { false }
    end
  end
end

Traits overwrite default attribute values and are helpful when you find yourself overriding these attributes to the same values consistently. Let's try leveraging our new traits by updating our previous test.

 test '.drafts should only return articles that are not published' do
    draft = create(:article, :draft)
    published_article = create(:article, :published)

    results = Article.drafts

    assert_includes(results, draft)
    assert_not_includes(results, published_article)
  end

We're making a trade-off here. We've introduced some convenience, but we've sacrificed some clarity. Reading this test in isolation, we can't really be sure what :published or :draft does; however, it can make writing tests easier while being slightly more difficult to grok what's going on.

You can even take it a step further by creating factories from traits. Let's say we've added the ability to mark an Article as featured so that it shows up in a prioritized location of our app.

FactoryBot.define do
  factory :article do
    # default attribute values defined here
    title { "Best article ever" }
    description  { "No really this is the best article I've ever read!" }
    published { false }
    featured { false }

    trait :published do
      published { true }
    end

    trait :draft do
      published { false }
    end

    trait :featured do
      featured { false }
    end

    # Defining new factories here
    factory :published_article, traits: %i[published]
    factory :draft, traits: %i[draft]
    factory :front_page_article, traits: %i[published featured]
  end
end

With these traits and factories added to our original article factory, the following methods can be called:

create(:published_article)
create(:draft)
create(:front_page_article)

Use your best judgement and create the factories that read well to you, and remember the trade-off you're making.

Setting unique fields with sequences

Let's say we've received a new feature requirement that each article must have a new unique title. We'll be covering how to address that in a code base in a separate blog post in a couple of days, but let's talk about how we can ensure our factories create unique titles in our test.

One option is to specify unique titles for every test case.

test "do something" do
  article = create(:article, title: "Test Article")
  article2 = create(:article, title: "Test Article 2")
end

But a common scenario in testing is needing to create batches of objects (i.e. for index pages). To do this, FactoryBot has a method called create_list.

create_list(:article, 5)

If we create these five articles using the convenience of create_list they would all end up with a title of our default value, "Best article ever". What a humble codebase we've found ourselves in.

FactoryBot's sequence method can help us here by providing each record with a unique index value that can be used to guarantee unique attributes. Here's what it looks like in action:

FactoryBot.define do
  factory :article do
    sequence(:title) { |n| "Article #{n}" }
    description  { "No really this is the best article I've ever read!" }
    published { false }
    featured { false }

    # traits and records abbeviated
  end
end

Now create_list(:article, 5) would create article with the titles "Article 0", "Article 1", "Article 2", and so on.

Tying records together

We forgot to tie articles to the user's creating them. Let's pretend we've added a user_id column to our articles table, and update our factory using an association.

Let's say the generated user factory that was created during our rails generate model command looks something like this:

FactoryBot.define do
  factory :user do
    sequence(:email) { |n| "test#{n}@example.com" }
    password { "password" }
    first_name { "John" }
    last_name { "Dorian" }
  end
end

We can automatically create user records when creating articles by using an association. If articles are tied to a user belongs_to :user, then the following user method will be available to us.

FactoryBot.define do
  factory :article do
    user
    sequence(:title) { |n| "Article #{n}" }
    description  { "No really this is the best article I've ever read!" }
    published { false }
    featured { false }

    # traits and records abbeviated
  end
end

Now calling create(:article) will create a User record first and set the articles user_id column. You can also create the user manually and pass it in.

user = create(:user)
article = create(:article, user: user)

Conclusion

This was a brief introduction into factory_bot_rails, but I hope you found it useful. There's a lot more to discuss here that I plan on covering in follow-up posts. For example:

  • How can you build an object without storing it in the database?
  • How does FactoryBot affect the time it takes to run your test suite?
  • How much information should we set in our default attributes?
  • How can we randomize these attributes with realistic data?
  • How could we leverage our factories to create local data for working in development?
  • How does FactoryBot handle polymorphic relationships?
  • Why FactoryBot, why not fixtures?

We'll answer all of these in due time, but if you're interested in reading ahead the factory bot GitHub repo has some excellent documenation for getting started.

Newsletter

I'm working on sending out a weekly newsletter. I'll make it as easy as possible to unsubscribe at any time.