Scenario-Based Test Data Fixtures
FakeDataDSL Scenarios let you define complex, interconnected test data with named references, lazy evaluation, and inheritance. Think of it as "Fixtures 2.0" that combines the best of Rails fixtures and Factory Bot.
Quick Start
# Define a scenario
FakeDataDSL::Scenarios.define(:e_commerce_checkout) do
# Define entities with 'let'
let(:buyer) { User.generate(role: "customer", verified: true) }
let(:seller) { User.generate(role: "merchant") }
let(:product) { Product.generate(seller: seller, price: 99.99) }
let(:cart) { ShoppingCart.generate(user: buyer, items: [product]) }
let(:order) { Order.generate(user: buyer, cart: cart, status: "pending") }
end
# Use in tests
RSpec.describe "Checkout" do
include_scenario :e_commerce_checkout
it "completes the order" do
expect(order.status).to eq("pending")
order.complete!
expect(order.status).to eq("completed")
end
end
Defining Scenarios
Basic Scenario
FakeDataDSL::Scenarios.define(:basic_user) do
let(:user) { User.generate }
let(:profile) { Profile.generate(user: user) }
end
With Schema References
FakeDataDSL::Scenarios.define(:blog_post) do
# Reference other scenarios
include_scenario :basic_user
let(:category) { Category.generate(name: "Technology") }
let(:post) {
Post.generate(
author: user, # From included scenario
category: category,
published: true
)
}
let(:comments) {
3.times.map { Comment.generate(post: post, author: User.generate) }
}
end
DSL File Syntax
You can also define scenarios in .dsl files:
# db/schemas/scenarios/e_commerce.dsl
@scenario "e_commerce_checkout"
Users:
buyer: User(role: "customer", verified: true)
seller: User(role: "merchant")
Products:
laptop: Product(seller: @seller, price: 999.99, stock: 10)
phone: Product(seller: @seller, price: 499.99, stock: 5)
Cart:
cart: ShoppingCart(user: @buyer)
items:
- CartItem(product: @laptop, quantity: 1)
- CartItem(product: @phone, quantity: 2)
Order:
pending_order: Order(user: @buyer, status: "pending")
from_cart: @cart
Using Scenarios
RSpec Integration
# spec/rails_helper.rb
require 'fake_data_dsl/scenarios'
RSpec.configure do |config|
config.include FakeDataDSL::Scenarios::RSpecHelpers
end
# spec/features/checkout_spec.rb
RSpec.describe "Checkout", type: :feature do
include_scenario :e_commerce_checkout
it "shows cart total" do
visit cart_path(cart)
expect(page).to have_content("$1,499.97") # laptop + 2 phones
end
it "processes payment" do
login_as buyer
visit checkout_path(order)
"Pay Now"
expect(order.reload.status).to eq("paid")
end
end
Minitest Integration
# test/test_helper.rb
require 'fake_data_dsl/scenarios'
class ActiveSupport::TestCase
include FakeDataDSL::Scenarios::MinitestHelpers
end
# test/integration/checkout_test.rb
class CheckoutTest < ActionDispatch::IntegrationTest
include_scenario :e_commerce_checkout
test "completes checkout" do
post checkout_path(order), params: { payment_method: "credit_card" }
assert_equal "completed", order.reload.status
end
end
Lazy Evaluation
Scenario entities are lazily evaluated - they're only created when accessed:
FakeDataDSL::Scenarios.define(:expensive_setup) do
let(:user) { User.generate } # Not created yet
let(:large_dataset) {
1000.times.map { Record.generate(user: user) }
} # Not created yet
end
RSpec.describe "Performance" do
include_scenario :expensive_setup
it "accesses user" do
expect(user).to be_present # NOW user is created
# large_dataset is never created in this test
end
it "accesses dataset" do
expect(large_dataset.count).to eq(1000) # NOW both are created
end
end
Entity References
Forward References
FakeDataDSL::Scenarios.define(:circular) do
let(:team) { Team.generate(lead: lead) } # References 'lead' defined below
let(:lead) { User.generate(team: team) } # References 'team' defined above
end
Cross-Scenario References
FakeDataDSL::Scenarios.define(:users) do
let(:admin) { User.generate(role: "admin") }
let(:regular) { User.generate(role: "user") }
end
FakeDataDSL::Scenarios.define(:permissions) do
include_scenario :users
let(:admin_permissions) { Permission.generate(user: admin, level: "full") }
let(:user_permissions) { Permission.generate(user: regular, level: "read") }
end
Scenario Inheritance
Extending Scenarios
FakeDataDSL::Scenarios.define(:base_user) do
let(:user) { User.generate }
let(:profile) { Profile.generate(user: user) }
end
FakeDataDSL::Scenarios.define(:admin_user, extends: :base_user) do
# Override user definition
let(:user) { User.generate(role: "admin", permissions: ["all"]) }
# Add new entities
let(:audit_log) { AuditLog.generate(admin: user) }
end
Multiple Inheritance
FakeDataDSL::Scenarios.define(:full_checkout,
extends: [:e_commerce_checkout, :payment_setup, :shipping_setup]
) do
let(:complete_order) {
Order.generate(
user: buyer,
cart: cart,
payment: payment_method,
shipping: shipping_address,
status: "complete"
)
}
end
Context and State
Shared Context
FakeDataDSL::Scenarios.define(:multi_tenant) do
let(:tenant) { Tenant.generate }
# All entities share the tenant context
with_context(tenant_id: -> { tenant.id }) do
let(:user) { User.generate } # Automatically gets tenant_id
let(:product) { Product.generate } # Automatically gets tenant_id
let(:order) { Order.generate(user: user) } # Automatically gets tenant_id
end
end
Conditional Entities
FakeDataDSL::Scenarios.define(:feature_flags) do
let(:user) { User.generate }
let(:premium_feature) {
if user.subscription == "premium"
PremiumFeature.generate(user: user)
end
}
let(:beta_feature) {
BetaFeature.generate(user: user) if ENV["ENABLE_BETA"]
}
end
Parameterized Scenarios
With Arguments
FakeDataDSL::Scenarios.define(:order_with_items) do |count: 3, status: "pending"|
let(:user) { User.generate }
let(:items) { count.times.map { OrderItem.generate } }
let(:order) { Order.generate(user: user, items: items, status: status) }
end
# Usage
RSpec.describe "Orders" do
include_scenario :order_with_items, count: 5, status: "shipped"
it "has correct items" do
expect(order.items.count).to eq(5)
expect(order.status).to eq("shipped")
end
end
Dynamic Scenarios
FakeDataDSL::Scenarios.define(:scaled_data) do |scale: 1|
let(:users) { (10 * scale).times.map { User.generate } }
let(:products) { (100 * scale).times.map { Product.generate } }
let(:orders) { (50 * scale).times.map { Order.generate } }
end
# Small dataset for unit tests
include_scenario :scaled_data, scale: 1
# Large dataset for load tests
include_scenario :scaled_data, scale: 100
Scenario Variants
Named Variants
FakeDataDSL::Scenarios.define(:user_states) do
variant(:active) do
let(:user) { User.generate(active: true, verified: true) }
end
variant(:pending) do
let(:user) { User.generate(active: false, verified: false) }
end
variant(:suspended) do
let(:user) { User.generate(active: false, suspended_at: Time.current) }
end
end
# Usage
include_scenario :user_states, variant: :suspended
Mode-Based Variants
FakeDataDSL::Scenarios.define(:edge_cases) do
variant(:happy_path, mode: :random) do
let(:user) { User.generate }
end
variant(:edge_cases, mode: :edge) do
let(:user) { User.generate } # Will use edge case values
end
variant(:hostile, mode: :hostile) do
let(:user) { User.generate } # Will use hostile values
end
end
Hooks
Setup and Teardown
FakeDataDSL::Scenarios.define(:with_hooks) do
before do
Rails.cache.clear
Sidekiq::Testing.inline!
end
after do
Sidekiq::Testing.fake!
end
let(:user) { User.generate }
end
Entity Callbacks
FakeDataDSL::Scenarios.define(:with_callbacks) do
let(:user) {
User.generate.tap do |u|
u.confirm! # Run after creation
u.update!(last_login: Time.current)
end
}
end
Database Integration
Creating Records
FakeDataDSL::Scenarios.define(:persisted) do
# create() persists to database
let(:user) { create(:user) }
let(:order) { create(:order, user: user) }
end
FakeDataDSL::Scenarios.define(:in_memory) do
# build() stays in memory
let(:user) { build(:user) }
let(:order) { build(:order, user: user) }
end
Transactional Scenarios
FakeDataDSL::Scenarios.define(:transactional) do
transactional! # Wrap in transaction, rollback after test
let(:user) { create(:user) }
let(:order) { create(:order) }
end
Debugging Scenarios
Inspect Scenario
scenario = FakeDataDSL::Scenarios.find(:e_commerce_checkout)
scenario.entities # => [:buyer, :seller, :product, :cart, :order]
scenario.dependencies # => { order: [:buyer, :cart], cart: [:buyer, :product], ... }
Visualization
# Generate dependency graph
FakeDataDSL::Scenarios.find(:e_commerce_checkout).to_dot
# => "digraph { buyer -> cart; cart -> order; ... }"
Logging
FakeDataDSL::Scenarios.configure do |config|
config.logger = Rails.logger
config.log_level = :debug
end
# Now see:
# [FakeDataDSL::Scenarios] Creating :user
# [FakeDataDSL::Scenarios] Creating :order (depends on :user, :cart)
API Reference
Scenarios.define
FakeDataDSL::Scenarios.define(name, = {}, &block)
Parameters:
name- Symbol name for the scenariooptions[:extends]- Parent scenario(s) to inherit fromblock- Scenario definition block
Scenario#let
let(name, &block)
Defines a lazy-evaluated entity.
Scenario#create
create(schema_name, overrides = {})
Creates and persists a record using the schema.
Scenario#build
build(schema_name, overrides = {})
Builds a record in memory (not persisted).
include_scenario
include_scenario(name, **)
Includes a scenario in a test, making all entities available.
Best Practices
1. Keep Scenarios Focused
# Good: Single purpose
FakeDataDSL::Scenarios.define(:checkout_ready) do
let(:buyer) { create(:user, :verified) }
let(:cart) { create(:cart, user: buyer, items: [product]) }
end
# Bad: Too much unrelated data
FakeDataDSL::Scenarios.define(:everything) do
let(:user) { ... }
let(:admin) { ... }
let(:product) { ... }
let(:report) { ... }
# ...50 more entities
end
2. Use Inheritance for Variations
FakeDataDSL::Scenarios.define(:base_order) do
let(:user) { create(:user) }
let(:order) { create(:order, user: user) }
end
FakeDataDSL::Scenarios.define(:pending_order, extends: :base_order) do
let(:order) { create(:order, user: user, status: "pending") }
end
FakeDataDSL::Scenarios.define(:shipped_order, extends: :base_order) do
let(:order) { create(:order, user: user, status: "shipped") }
end
3. Document Dependencies
FakeDataDSL::Scenarios.define(:complex_workflow) do
# This scenario sets up a complete order fulfillment workflow
# Dependencies:
# - buyer: Customer placing the order
# - seller: Merchant fulfilling the order
# - product: Item being purchased
# - order: The order connecting buyer and seller
# - shipment: Delivery tracking
let(:buyer) { ... }
let(:seller) { ... }
# ...
end
Troubleshooting
Circular Dependencies
# Error: Circular dependency detected: user -> team -> user
# Fix: Break the cycle with nil defaults
FakeDataDSL::Scenarios.define(:team_setup) do
let(:team) { create(:team, lead: nil) }
let(:user) { create(:user, team: team) }
after do
team.update!(lead: user)
end
end
Entity Not Found
# Error: Unknown entity: :admin
# Fix: Check includes
FakeDataDSL::Scenarios.define(:needs_admin) do
include_scenario :users # Make sure this defines :admin
let(:action) { AdminAction.create(admin: admin) }
end