Files
claude-engineering-plugin/plugins/compound-engineering/skills/dhh-rails-style/references/testing.md
Kieran Klaassen 141bbb42cb [2.16.0] Consolidate DHH styles and add /feature-video command
- Merge dhh-ruby-style into dhh-rails-style for comprehensive Rails conventions
- Add testing.md reference covering Rails testing patterns
- Add /feature-video command for recording PR demo videos
- Update docs and component counts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 10:25:32 -08:00

6.9 KiB

Testing - DHH Rails Style

Core Philosophy

"Minitest with fixtures - simple, fast, deterministic." The approach prioritizes pragmatism over convention.

Why Minitest Over RSpec

  • Simpler: Less DSL magic, plain Ruby assertions
  • Ships with Rails: No additional dependencies
  • Faster boot times: Less overhead
  • Plain Ruby: No specialized syntax to learn

Fixtures as Test Data

Rather than factories, fixtures provide preloaded data:

  • Loaded once, reused across tests
  • No runtime object creation overhead
  • Explicit relationship visibility
  • Deterministic IDs for easier debugging

Fixture Structure

# test/fixtures/users.yml
david:
  identity: david
  account: basecamp
  role: admin

jason:
  identity: jason
  account: basecamp
  role: member

# test/fixtures/rooms.yml
watercooler:
  name: Water Cooler
  creator: david
  direct: false

# test/fixtures/messages.yml
greeting:
  body: Hello everyone!
  room: watercooler
  creator: david

Using Fixtures in Tests

test "sending a message" do
  user = users(:david)
  room = rooms(:watercooler)

  # Test with fixture data
end

Dynamic Fixture Values

ERB enables time-sensitive data:

recent_card:
  title: Recent Card
  created_at: <%= 1.hour.ago %>

old_card:
  title: Old Card
  created_at: <%= 1.month.ago %>

Test Organization

Unit Tests

Verify business logic using setup blocks and standard assertions:

class CardTest < ActiveSupport::TestCase
  setup do
    @card = cards(:one)
    @user = users(:david)
  end

  test "closing a card creates a closure" do
    assert_difference -> { Card::Closure.count } do
      @card.close(creator: @user)
    end

    assert @card.closed?
    assert_equal @user, @card.closure.creator
  end

  test "reopening a card destroys the closure" do
    @card.close(creator: @user)

    assert_difference -> { Card::Closure.count }, -1 do
      @card.reopen
    end

    refute @card.closed?
  end
end

Integration Tests

Test full request/response cycles:

class CardsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:david)
    sign_in @user
  end

  test "closing a card" do
    card = cards(:one)

    post card_closure_path(card)

    assert_response :success
    assert card.reload.closed?
  end

  test "unauthorized user cannot close card" do
    sign_in users(:guest)
    card = cards(:one)

    post card_closure_path(card)

    assert_response :forbidden
    refute card.reload.closed?
  end
end

System Tests

Browser-based tests using Capybara:

class MessagesTest < ApplicationSystemTestCase
  test "sending a message" do
    sign_in users(:david)
    visit room_path(rooms(:watercooler))

    fill_in "Message", with: "Hello, world!"
    click_button "Send"

    assert_text "Hello, world!"
  end

  test "editing own message" do
    sign_in users(:david)
    visit room_path(rooms(:watercooler))

    within "#message_#{messages(:greeting).id}" do
      click_on "Edit"
    end

    fill_in "Message", with: "Updated message"
    click_button "Save"

    assert_text "Updated message"
  end

  test "drag and drop card to new column" do
    sign_in users(:david)
    visit board_path(boards(:main))

    card = find("#card_#{cards(:one).id}")
    target = find("#column_#{columns(:done).id}")

    card.drag_to target

    assert_selector "#column_#{columns(:done).id} #card_#{cards(:one).id}"
  end
end

Advanced Patterns

Time Testing

Use travel_to for deterministic time-dependent assertions:

test "card expires after 30 days" do
  card = cards(:one)

  travel_to 31.days.from_now do
    assert card.expired?
  end
end

External API Testing with VCR

Record and replay HTTP interactions:

test "fetches user data from API" do
  VCR.use_cassette("user_api") do
    user_data = ExternalApi.fetch_user(123)

    assert_equal "John", user_data[:name]
  end
end

Background Job Testing

Assert job enqueueing and email delivery:

test "closing card enqueues notification job" do
  card = cards(:one)

  assert_enqueued_with(job: NotifyWatchersJob, args: [card]) do
    card.close
  end
end

test "welcome email is sent on signup" do
  assert_emails 1 do
    Identity.create!(email: "new@example.com")
  end
end

Testing Turbo Streams

test "message creation broadcasts to room" do
  room = rooms(:watercooler)

  assert_turbo_stream_broadcasts [room, :messages] do
    room.messages.create!(body: "Test", creator: users(:david))
  end
end

Testing Principles

1. Test Observable Behavior

Focus on what the code does, not how it does it:

# ❌ Testing implementation
test "calls notify method on each watcher" do
  card.expects(:notify).times(3)
  card.close
end

# ✅ Testing behavior
test "watchers receive notifications when card closes" do
  assert_difference -> { Notification.count }, 3 do
    card.close
  end
end

2. Don't Mock Everything

# ❌ Over-mocked test
test "sending message" do
  room = mock("room")
  user = mock("user")
  message = mock("message")

  room.expects(:messages).returns(stub(create!: message))
  message.expects(:broadcast_create)

  MessagesController.new.create
end

# ✅ Test the real thing
test "sending message" do
  sign_in users(:david)
  post room_messages_url(rooms(:watercooler)),
    params: { message: { body: "Hello" } }

  assert_response :success
  assert Message.exists?(body: "Hello")
end

3. Tests Ship with Features

Same commit, not TDD-first but together. Neither before (strict TDD) nor after (deferred testing).

4. Security Fixes Always Include Regression Tests

Every security fix must include a test that would have caught the vulnerability.

5. Integration Tests Validate Complete Workflows

Don't just test individual pieces - test that they work together.

File Organization

test/
├── controllers/         # Integration tests for controllers
├── fixtures/           # YAML fixtures for all models
├── helpers/            # Helper method tests
├── integration/        # API integration tests
├── jobs/               # Background job tests
├── mailers/            # Mailer tests
├── models/             # Unit tests for models
├── system/             # Browser-based system tests
└── test_helper.rb      # Test configuration

Test Helper Setup

# test/test_helper.rb
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"

class ActiveSupport::TestCase
  fixtures :all

  parallelize(workers: :number_of_processors)
end

class ActionDispatch::IntegrationTest
  include SignInHelper
end

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome
end

Sign In Helper

# test/support/sign_in_helper.rb
module SignInHelper
  def sign_in(user)
    session = user.identity.sessions.create!
    cookies.signed[:session_id] = session.id
  end
end