Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
104: Factory Bot for Test Data
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
:orderfactory that defaults to apendingstatus and atotal_amountof 0. - An association to a
:userfactory. - A
:paidtrait that changes the status to "paid" and sets thetotal_amountto 50.00. - A
:shippedtrait that changes the status to "shipped". This trait should depend on the:paidtrait (meaning a shipped order must also be paid).
Write the FactoryBot.define block for both the User and Order factories to make this possible.
There are no comments for now.