Use Fixtures for Test Data
Use Rails fixtures instead of factories (FactoryBot). Create a comprehensive fixture set that represents your domain, with deterministic IDs for predictable ordering.
Why
- Speed: Fixtures load once per test run, factories create objects per test
- Realism: Fixtures represent a complete, coherent dataset
- Simplicity: No factory DSL to learn or maintain
- Predictability: Deterministic IDs make debugging easier
- Discovery: All test data is visible in YAML files
Fixture Organization
test/fixtures/
├── accounts.yml
├── users.yml
├── boards.yml
├── cards.yml
├── comments.yml
├── events.yml
├── sessions.yml
└── files/ # Binary fixtures (images, etc.)
└── avatar.pngBasic Fixture Patterns
Named Fixtures with References
# test/fixtures/users.yml
david:
name: David
email: david@example.com
account: primary
role: admin
kevin:
name: Kevin
email: kevin@example.com
account: primary
role: memberUUID Primary Keys
For apps using UUIDs, generate deterministic IDs:
# test/fixtures/cards.yml
logo:
id: <%= ActiveRecord::FixtureSet.identify("logo", :uuid) %>
title: The logo isn't big enough
board: writebook
creator: david
status: published
created_at: <%= 1.week.ago %>Foreign Key References
Use fixture names directly (Rails resolves them):
# test/fixtures/comments.yml
first_comment:
card: logo # References cards(:logo)
creator: kevin # References users(:kevin)
body: I agree!For UUID foreign keys, use the _uuid suffix convention:
# test/fixtures/events.yml
logo_published:
id: <%= ActiveRecord::FixtureSet.identify("logo_published", :uuid) %>
board: writebook_uuid
creator: david_uuid
eventable: logo (Card)Polymorphic Associations
# test/fixtures/notifications.yml
logo_notification:
user: kevin
source: logo_published (Event) # Type in parenthesesJSON/JSONB Columns
# test/fixtures/events.yml
card_assignment:
particulars: <%= { assignee_ids: [ActiveRecord::FixtureSet.identify("kevin", :uuid)] }.to_json %>Accessing Fixtures in Tests
class CardTest < ActiveSupport::TestCase
test "card has a title" do
card = cards(:logo)
assert_equal "The logo isn't big enough", card.title
end
test "card belongs to board" do
assert_equal boards(:writebook), cards(:logo).board
end
endMultiple Fixtures at Once
test "all cards are valid" do
cards(:logo, :layout, :draft).each do |card|
assert card.valid?
end
endTest Setup with Current
Set up Current attributes in test helper:
# test/test_helper.rb
class ActiveSupport::TestCase
fixtures :all
setup do
Current.account = accounts(:primary)
end
teardown do
Current.reset_all
end
endSession/User Context
# test/test_helpers/session_test_helper.rb
module SessionTestHelper
def sign_in_as(user_or_fixture)
user = user_or_fixture.is_a?(Symbol) ? users(user_or_fixture) : user_or_fixture
Current.session = sessions(user.name.parameterize.to_sym)
Current.user = user
end
def with_current_user(user)
original = Current.user
Current.user = user
yield
ensure
Current.user = original
end
endTesting Concerns
Test concerns in files mirroring their location:
test/models/
├── card_test.rb
├── card/
│ ├── closeable_test.rb # Tests Card::Closeable
│ ├── searchable_test.rb # Tests Card::Searchable
│ └── watchable_test.rb # Tests Card::Watchable
└── concerns/
└── mentions_test.rb # Tests shared Mentions concern# test/models/card/closeable_test.rb
class Card::CloseableTest < ActiveSupport::TestCase
setup do
Current.session = sessions(:david)
end
test "close creates a closure" do
card = cards(:logo)
assert_difference -> { Closure.count }, 1 do
card.close
end
assert card.closed?
end
test "reopen destroys the closure" do
card = cards(:shipping) # Already closed in fixtures
assert_difference -> { Closure.count }, -1 do
card.reopen
end
assert_not card.closed?
end
endIntegration Test Patterns
class CardsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in_as :david
end
test "create a new card" do
assert_difference -> { Card.count }, 1 do
post board_cards_path(boards(:writebook)),
params: { card: { title: "New card" } }
end
assert_redirected_to card_path(Card.last)
end
test "unauthorized user cannot access card" do
sign_in_as :other_account_user
get card_path(cards(:logo))
assert_response :not_found
end
endTesting Turbo Streams
test "closing card returns turbo stream" do
card = cards(:logo)
post card_closure_path(card), as: :turbo_stream
assert_turbo_stream action: :replace, target: dom_id(card, :card_container)
endTesting Jobs
test "closing card enqueues notification job" do
card = cards(:logo)
assert_enqueued_with(job: NotifyRecipientsJob) do
card.close
end
end
test "job calls model method" do
card = cards(:logo)
perform_enqueued_jobs only: Card::ActivitySpike::DetectionJob do
card.update!(last_active_at: Time.current)
end
assert card.reload.stalled?
endRules
- Use fixtures, not factories
- Create a coherent dataset that represents real usage
- Use deterministic IDs for predictable ordering
- Reference fixtures by name in tests
- Set up
Currentattributes in test setup - Mirror concern location in test file structure
- Use
assert_enqueued_withfor job testing - Use
assert_turbo_streamfor Turbo response testing