Skip to Content
Course content

104: Factory Bot for Test Data

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time when I'm reviewing PRs from developers moving into Ruby on Rails: they treat Factory Bot as if it's just a "shorthand" for Model.create. They think the goal is simply to avoid typing out a long hash of attributes every time they need a record in a test.

The Myth: Factories are just a prettier way to call .create

If you think Factory Bot is just about saving keystrokes, you'll end up with "Factory Bloat." You'll find yourself manually building every dependency for every single test. Look at this common pattern:

# The "Shorthand" Approach (Still painful)
it "allows a premium user to access the dashboard" do
  plan = create(:plan, name: "Premium", price: 99)
  user = create(:user, plan: plan, active: true)
  
  expect(user.can_access_dashboard?).to be true
end

This isn't actually solving the problem. You're still managing the relationship between the user and the plan inside your test. Your test is now cluttered with setup logic that has nothing to do with the actual behavior you're testing (the dashboard access). If you change how a "Premium" user is defined tomorrow, you have to hunt down every single test that manually links a user to a premium plan.

The Reality: Managing State and Object Graphs

The real power of Factory Bot isn't in creating a single object; it's in defining states and associations so your tests can stay focused on the "act" rather than the "arrange."

Instead of manually wiring things together in the test, we move that logic into the factory using associations and traits. Traits are, in my opinion, the secret weapon of a clean test suite. They let you define "flavors" of an object.

FactoryBot.define do
  factory :plan do
    name { "Basic" }
    price { 0 }

    trait :premium do
      name { "Premium" }
      price { 99 }
    end
  end

  factory :user do
    email { "user@example.com" }
    active { true }
    association :plan # This creates a basic plan by default

    trait :premium do
      association :plan, :premium # Use the premium trait of the plan factory
    end

    trait :inactive do
      active { false }
    end
  end
end

Now, look how much the test changes. We've moved the "knowledge" of what makes a user premium out of the test and into the factory:

it "allows a premium user to access the dashboard" do
  user = create(:user, :premium) # One line. Intent is crystal clear.
  
  expect(user.can_access_dashboard?).to be true
end

I want you to notice that the test no longer cares about the Plan model. It only cares that the user is premium. If you later decide that premium users also need a specific AccountManager assigned to them, you update the :premium trait in one place, and every single test in your suite is instantly updated.

One last tip: be careful with create. It hits the database, which slows down your suite. Whenever you can, use build (which creates the object in memory) or build_stubbed (which fakes the ID and associations). If your test doesn't actually need the record saved to disk to work, don't save it.




📋 Practical Task

Implementing an Order State Machine with Factory Bot

You are working on an E-commerce application. You need to set up factories for an Order model. An Order belongs to a User and has a status (which can be pending, paid, or shipped) and a total_amount.

Your task: Create a Factory Bot definition that fulfills these requirements:

  • A base :order factory that defaults to a pending status and a total_amount of 0.
  • An association to a :user factory.
  • A :paid trait that changes the status to "paid" and sets the total_amount to 50.00.
  • A :shipped trait that changes the status to "shipped". This trait should depend on the :paid trait (meaning a shipped order must also be paid).

Write the FactoryBot.define block for both the User and Order factories to make this possible.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.