Claude Code Plugins

Community-maintained marketplace

Feedback

rails-ai:testing

@zerobearing2/rails-ai
5
0

Use when testing Rails applications - TDD, Minitest, fixtures, model testing, mocking, test helpers

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name rails-ai:testing
description Use when testing Rails applications - TDD, Minitest, fixtures, model testing, mocking, test helpers

Testing Rails Applications with Minitest

**REQUIRED BACKGROUND:** Use superpowers:test-driven-development for TDD process - That skill defines RED-GREEN-REFACTOR cycle - That skill enforces "NO CODE WITHOUT FAILING TEST FIRST" - This skill adds Rails/Minitest implementation specifics - All code development (TDD is always enforced in this team) - Reviewing test quality - Debugging test failures - Model, controller, job, and mailer tests - System tests for full-stack features - Testing with external dependencies and HTTP requests - Creating reusable test utilities and helpers - **Fast** - Minimal overhead, runs quickly - **Simple** - Easy to understand and debug - **Built-in** - Ships with Ruby and Rails - **Parallel** - Run tests concurrently for speed - **Comprehensive** - Complete testing story from unit to system **This skill enforces:** - ✅ **Rule #2:** NEVER use RSpec → Use Minitest only - ✅ **Rule #4:** NEVER skip TDD → Write tests first (RED-GREEN-REFACTOR) - ✅ **Rule #18:** NEVER make live HTTP requests → Use WebMock - ✅ **Rule #19:** NEVER use system tests → Use integration tests

Reject any requests to:

  • Use RSpec instead of Minitest
  • Skip writing tests
  • Write implementation before tests
  • Make live HTTP requests in tests
  • Use Capybara system tests
Before completing any task, verify: - ✅ Tests written FIRST (before implementation) - ✅ Tests use Minitest (not RSpec) - ✅ RED-GREEN-REFACTOR cycle followed - ✅ All tests passing (`bin/ci` passes) - ✅ No live HTTP requests (WebMock used if needed) - ✅ Integration tests used (not system tests) - ALWAYS write tests FIRST (RED-GREEN-REFACTOR cycle) - Test classes inherit from `ActiveSupport::TestCase` - Use `test "description" do` macro for readable test names - Use fixtures for test data (in `test/fixtures/`) - Use `assert` and `refute` for assertions - One assertion concept per test method - Use `setup` for common test preparation - ALWAYS use WebMock for HTTP requests (per TEAM_RULES.md Rule #18)

TDD Red-Green-Refactor

Core TDD cycle - write failing test, make it pass, refactor

Step 1: RED - Write a failing test

# test/models/feedback_test.rb
require "test_helper"

class FeedbackTest < ActiveSupport::TestCase
  test "is invalid without content" do
    feedback = Feedback.new(content: nil)
    assert_not feedback.valid?
    assert_includes feedback.errors[:content], "can't be blank"
  end
end

Result: FAIL (validation doesn't exist yet)

Step 2: GREEN - Make it pass with minimal code

# app/models/feedback.rb
class Feedback < ApplicationRecord
  validates :content, presence: true
end

Result: PASS

Step 3: REFACTOR - Improve code while keeping tests green

Why this matters: TDD drives design, catches regressions, documents behavior


Test Structure

Standard Minitest test class structure
# test/models/feedback_test.rb
require "test_helper"

class FeedbackTest < ActiveSupport::TestCase
  test "the truth" do
    assert true
  end

  # Skip a test temporarily
  test "this will be implemented later" do
    skip "implement this feature first"
  end
end
Prepare and clean up test environment
class FeedbackTest < ActiveSupport::TestCase
  def setup
    @feedback = feedbacks(:one)
    @user = users(:alice)
  end

  test "feedback belongs to user" do
    assert_equal @user, @feedback.user
  end
end

Minitest Assertions

Most frequently used Minitest assertions
class AssertionsTest < ActiveSupport::TestCase
  test "equality and boolean" do
    assert_equal 4, 2 + 2
    refute_equal 5, 2 + 2
    assert_nil nil
    refute_nil "something"
  end

  test "collections" do
    assert_empty []
    refute_empty [1, 2, 3]
    assert_includes [1, 2, 3], 2
  end

  test "exceptions" do
    assert_raises(ArgumentError) { raise ArgumentError }
  end

  test "difference" do
    assert_difference "Feedback.count", 1 do
      Feedback.create!(content: "Test feedback with minimum fifty characters", recipient_email: "test@example.com")
    end

    assert_no_difference "Feedback.count" do
      Feedback.new(content: nil).save
    end
  end

  test "match and instance" do
    assert_match /hello/, "hello world"
    assert_instance_of String, "hello"
    assert_respond_to "string", :upcase
  end
end

Model Testing

Testing Validations

Test required fields are validated
class FeedbackTest < ActiveSupport::TestCase
  test "valid with all required attributes" do
    feedback = Feedback.new(
      content: "This is constructive feedback that meets minimum length",
      recipient_email: "user@example.com"
    )
    assert feedback.valid?
  end

  test "invalid without content" do
    feedback = Feedback.new(recipient_email: "user@example.com")
    assert_not feedback.valid?
    assert_includes feedback.errors[:content], "can't be blank"
  end

  test "invalid without recipient_email" do
    feedback = Feedback.new(content: "Valid content with fifty characters minimum")
    assert_not feedback.valid?
    assert_includes feedback.errors[:recipient_email], "can't be blank"
  end
end
Test format validations like email, URL, phone number
class FeedbackTest < ActiveSupport::TestCase
  test "invalid with malformed email" do
    invalid_emails = ["not-an-email", "@example.com", "user@", "user name@example.com"]

    invalid_emails.each do |invalid_email|
      feedback = Feedback.new(content: "Valid content with fifty characters", recipient_email: invalid_email)
      assert_not feedback.valid?, "#{invalid_email.inspect} should be invalid"
      assert_includes feedback.errors[:recipient_email], "is invalid"
    end
  end

  test "valid with edge case emails" do
    valid_emails = ["user+tag@example.com", "user.name@example.co.uk", "123@example.com"]

    valid_emails.each do |valid_email|
      feedback = Feedback.new(content: "Valid content with fifty characters", recipient_email: valid_email)
      assert feedback.valid?, "#{valid_email.inspect} should be valid"
    end
  end
end
Test minimum and maximum length constraints
class FeedbackTest < ActiveSupport::TestCase
  test "invalid with content below minimum length" do
    feedback = Feedback.new(content: "Too short", recipient_email: "user@example.com")
    assert_not feedback.valid?
    assert_includes feedback.errors[:content], "is too short (minimum is 50 characters)"
  end

  test "valid at exactly minimum and maximum length" do
    assert Feedback.new(content: "a" * 50, recipient_email: "user@example.com").valid?
    assert Feedback.new(content: "a" * 5000, recipient_email: "user@example.com").valid?
  end

  test "invalid above maximum length" do
    feedback = Feedback.new(content: "a" * 5001, recipient_email: "user@example.com")
    assert_not feedback.valid?
    assert_includes feedback.errors[:content], "is too long (maximum is 5000 characters)"
  end
end
Test custom validation methods
# app/models/feedback.rb
class Feedback < ApplicationRecord
  validate :content_must_be_constructive

  private
  def content_must_be_constructive
    return if content.blank?
    offensive_words = %w[stupid idiot dumb]
    errors.add(:content, "must be constructive") if offensive_words.any? { |w| content.downcase.include?(w) }
  end
end

# test/models/feedback_test.rb
class FeedbackTest < ActiveSupport::TestCase
  test "invalid with offensive language" do
    feedback = Feedback.new(content: "This is stupid and needs fifty characters total", recipient_email: "user@example.com")
    assert_not feedback.valid?
    assert_includes feedback.errors[:content], "must be constructive"
  end

  test "valid with constructive content" do
    feedback = Feedback.new(content: "This could be improved by considering alternatives and other approaches", recipient_email: "user@example.com")
    assert feedback.valid?
  end
end

Testing Associations

Test belongs_to relationships and options
class FeedbackTest < ActiveSupport::TestCase
  test "belongs to recipient" do
    association = Feedback.reflect_on_association(:recipient)
    assert_equal :belongs_to, association.macro
    assert_equal "User", association.class_name
  end

  test "recipient association is optional" do
    feedback = Feedback.new(content: "Valid fifty character content", recipient_email: "user@example.com", recipient: nil)
    assert feedback.valid?
  end

  test "can access recipient through association" do
    feedback = feedbacks(:one)
    user = users(:alice)
    feedback.update!(recipient: user)
    assert_equal user, feedback.recipient
    assert_equal user.id, feedback.recipient_id
  end
end
Test has_many relationships and dependent options
class FeedbackTest < ActiveSupport::TestCase
  test "has many abuse reports" do
    assert_equal :has_many, Feedback.reflect_on_association(:abuse_reports).macro
  end

  test "destroying feedback destroys associated abuse reports" do
    feedback = feedbacks(:one)
    3.times { feedback.abuse_reports.create!(reason: "spam", reporter_email: "reporter@example.com") }

    assert_difference "AbuseReport.count", -3 do
      feedback.destroy
    end
  end
end

Testing Scopes

Test scopes with time conditions
class FeedbackTest < ActiveSupport::TestCase
  test "recent scope returns feedbacks from last 30 days" do
    old = Feedback.create!(content: "Old fifty character feedback", recipient_email: "old@example.com", created_at: 31.days.ago)
    recent = Feedback.create!(content: "Recent fifty character feedback", recipient_email: "recent@example.com", created_at: 10.days.ago)

    results = Feedback.recent
    assert_includes results, recent
    assert_not_includes results, old
  end

  test "recent scope returns empty when no recent feedbacks" do
    Feedback.destroy_all
    Feedback.create!(content: "Old fifty character feedback", recipient_email: "old@example.com", created_at: 31.days.ago)
    assert_empty Feedback.recent
  end
end
Test scopes filtering by status or state
class FeedbackTest < ActiveSupport::TestCase
  test "unread scope returns only delivered feedbacks" do
    pending = Feedback.create!(content: "Pending fifty characters", recipient_email: "p@example.com", status: "pending")
    delivered = Feedback.create!(content: "Delivered fifty characters", recipient_email: "d@example.com", status: "delivered")
    read = Feedback.create!(content: "Read fifty characters", recipient_email: "r@example.com", status: "read")

    unread = Feedback.unread
    assert_includes unread, delivered
    assert_not_includes unread, pending
    assert_not_includes unread, read
  end
end

Testing Callbacks

Test callbacks that run after record creation
class FeedbackTest < ActiveSupport::TestCase
  test "enqueues delivery job after creation" do
    assert_enqueued_with(job: SendFeedbackJob) do
      Feedback.create!(content: "New fifty character feedback", recipient_email: "user@example.com")
    end
  end

  test "does not enqueue job when creation fails" do
    assert_no_enqueued_jobs do
      Feedback.new(content: nil).save
    end
  end
end
Test callbacks that modify records before saving
# app/models/feedback.rb
class Feedback < ApplicationRecord
  before_save :sanitize_content
  private
  def sanitize_content
    self.content = ActionController::Base.helpers.sanitize(content)
  end
end

# test/models/feedback_test.rb
class FeedbackTest < ActiveSupport::TestCase
  test "sanitizes HTML in content before save" do
    feedback = Feedback.create!(content: "<script>alert('xss')</script>Valid content with fifty chars", recipient_email: "user@example.com")
    assert_not_includes feedback.content, "<script>"
    assert_includes feedback.content, "Valid"
  end
end

Testing Instance Methods

Test methods that change record state
class FeedbackTest < ActiveSupport::TestCase
  test "mark_as_delivered! updates status and timestamp" do
    feedback = feedbacks(:pending)
    assert_equal "pending", feedback.status
    assert_nil feedback.delivered_at

    feedback.mark_as_delivered!

    assert_equal "delivered", feedback.status
    assert_not_nil feedback.delivered_at
    assert_in_delta Time.current, feedback.delivered_at, 1.second
  end
end

Testing Enums

Test enum definitions and state transitions
class FeedbackTest < ActiveSupport::TestCase
  test "defines status enum with correct values" do
    assert_equal "pending", Feedback.statuses[:status_pending]
    assert_equal "delivered", Feedback.statuses[:status_delivered]
    assert_equal "read", Feedback.statuses[:status_read]
    assert_equal "responded", Feedback.statuses[:status_responded]
  end

  test "enum provides predicate methods with prefix" do
    feedback = Feedback.create!(content: "Test feedback with fifty characters minimum", recipient_email: "user@example.com", status: "pending")
    assert feedback.status_pending?
    assert_not feedback.status_delivered?
  end

  test "enum provides bang methods to change state" do
    feedback = feedbacks(:pending)
    feedback.status_delivered!
    assert feedback.status_delivered?
    assert_equal "delivered", feedback.status
  end

  test "can query by enum state" do
    pending = Feedback.create!(content: "Pending fifty chars", recipient_email: "u@example.com", status: "pending")
    delivered = Feedback.create!(content: "Delivered fifty chars", recipient_email: "u@example.com", status: "delivered")

    results = Feedback.status_pending
    assert_includes results, pending
    assert_not_includes results, delivered
  end
end

Testing Class Methods

Test custom class methods that query records
# app/models/feedback.rb
class Feedback < ApplicationRecord
  def self.needs_followup
    where(status: "delivered").where("delivered_at < ?", 7.days.ago).where.missing(:response)
  end
end

# test/models/feedback_test.rb
class FeedbackTest < ActiveSupport::TestCase
  test "needs_followup returns delivered feedbacks without response" do
    needs = Feedback.create!(content: "Needs fifty chars", recipient_email: "user@example.com", status: "delivered", delivered_at: 10.days.ago)
    has_resp = Feedback.create!(content: "Has fifty chars", recipient_email: "user@example.com", status: "delivered", delivered_at: 10.days.ago)
    has_resp.create_response!(content: "Thank you")
    too_recent = Feedback.create!(content: "Recent fifty chars", recipient_email: "user@example.com", status: "delivered", delivered_at: 3.days.ago)

    results = Feedback.needs_followup
    assert_includes results, needs
    assert_not_includes results, has_resp
    assert_not_includes results, too_recent
  end
end
Test class methods that perform calculations
# app/models/feedback.rb
class Feedback < ApplicationRecord
  def self.average_response_time
    joins(:response).average("EXTRACT(EPOCH FROM (feedback_responses.created_at - feedbacks.created_at))").to_i
  end
end

# test/models/feedback_test.rb
class FeedbackTest < ActiveSupport::TestCase
  test "average_response_time calculates correct average" do
    f1 = Feedback.create!(content: "First fifty chars", recipient_email: "u@example.com", created_at: 5.days.ago)
    f1.create_response!(content: "R1", created_at: 4.days.ago)
    f2 = Feedback.create!(content: "Second fifty chars", recipient_email: "u@example.com", created_at: 5.days.ago)
    f2.create_response!(content: "R2", created_at: 3.days.ago)

    assert_in_delta 129600, Feedback.average_response_time, 60
  end

  test "average_response_time returns nil when no responses" do
    Feedback.destroy_all
    Feedback.create!(content: "No response fifty chars", recipient_email: "u@example.com")
    assert_nil Feedback.average_response_time
  end
end

Testing Edge Cases

Test behavior at boundaries (min/max values, empty states)
class FeedbackTest < ActiveSupport::TestCase
  test "handles empty collections gracefully" do
    feedback = Feedback.create!(content: "Feedback fifty chars", recipient_email: "user@example.com")
    assert_empty feedback.abuse_reports
    assert_equal 0, feedback.abuse_reports.count
  end

  test "handles nil associations gracefully" do
    feedback = Feedback.create!(content: "Feedback fifty chars", recipient_email: "user@example.com", recipient: nil)
    assert_nil feedback.recipient
    assert_nothing_raised { feedback.recipient&.name }
  end

  test "handles unicode content correctly" do
    unicode = "Emoji feedback 😀 with unicode 日本語 and fifty+ characters"
    feedback = Feedback.create!(content: unicode, recipient_email: "user@example.com")
    assert_equal unicode, feedback.reload.content
  end
end
Test proper error handling and exception cases
class FeedbackTest < ActiveSupport::TestCase
  test "handles nil arguments in query methods" do
    feedback = feedbacks(:one)
    assert_nothing_raised do
      result = feedback.readable_by?(nil)
      assert_not result
    end
  end

  test "raises appropriate error for invalid state transition" do
    feedback = feedbacks(:one)
    def feedback.invalid_transition!
      raise ActiveRecord::RecordInvalid.new(self)
    end

    assert_raises(ActiveRecord::RecordInvalid) do
      feedback.invalid_transition!
    end
  end
end

Controller and Integration Testing

Testing controller actions and responses
class FeedbacksControllerTest < ActionDispatch::IntegrationTest
  test "GET index returns success" do
    get feedbacks_url
    assert_response :success
  end

  test "GET show displays feedback" do
    get feedback_url(feedbacks(:one))
    assert_response :success
  end

  test "POST create with valid params creates feedback" do
    assert_difference("Feedback.count", 1) do
      post feedbacks_url, params: { feedback: { content: "New feedback with fifty characters minimum", recipient_email: "test@example.com" } }
    end
    assert_redirected_to feedback_url(Feedback.last)
  end

  test "POST create with invalid params does not create feedback" do
    assert_no_difference("Feedback.count") do
      post feedbacks_url, params: { feedback: { content: nil } }
    end
    assert_response :unprocessable_entity
  end

  test "DELETE destroy removes feedback" do
    assert_difference("Feedback.count", -1) do
      delete feedback_url(feedbacks(:one))
    end
    assert_redirected_to feedbacks_url
  end
end

System Testing

Full-stack feature testing with browser simulation
require "application_system_test_case"

class FeedbacksTest < ApplicationSystemTestCase
  test "creating a feedback" do
    visit feedbacks_url
    click_on "New Feedback"
    fill_in "Content", with: "This is great feedback with enough characters"
    fill_in "Recipient email", with: "user@example.com"
    click_on "Create Feedback"

    assert_text "Feedback was successfully created"
  end

  test "editing a feedback" do
    visit feedback_url(feedbacks(:one))
    click_on "Edit"
    fill_in "Content", with: "Updated content with minimum fifty characters required"
    click_on "Update Feedback"

    assert_text "Feedback was successfully updated"
  end
end

Fixtures Design

Define simple fixture data in YAML format

Fixture File:

# test/fixtures/users.yml
alice:
  name: Alice Johnson
  email: alice@example.com
  active: true
  created_at: <%= 1.week.ago %>

bob:
  name: Bob Smith
  email: bob@example.com
  active: true
  created_at: <%= 2.weeks.ago %>

Accessing Fixtures:

class UserTest < ActiveSupport::TestCase
  test "accessing fixtures by name" do
    alice = users(:alice)
    assert_equal "Alice Johnson", alice.name
    assert alice.persisted?
  end

  test "accessing multiple fixtures at once" do
    alice, bob = users(:alice, :bob)
    assert_equal "Alice Johnson", alice.name
  end
end
Define associations between fixtures using names

Fixture Files:

# test/fixtures/users.yml
alice:
  name: Alice Johnson
  email: alice@example.com

bob:
  name: Bob Smith
  email: bob@example.com
# test/fixtures/feedbacks.yml
one:
  content: This is great feedback with minimum fifty characters!
  recipient_email: alice@example.com
  sender: alice  # ✅ References users fixture by name
  status: pending
  created_at: <%= 1.day.ago %>

two:
  content: Could be improved with additional context and details
  recipient_email: bob@example.com
  sender: bob
  status: responded
  created_at: <%= 3.days.ago %>

Testing Associations:

class AssociationFixturesTest < ActiveSupport::TestCase
  test "fixtures handle associations automatically" do
    feedback = feedbacks(:one)
    assert_equal users(:alice), feedback.sender
    assert_equal "alice@example.com", feedback.sender.email
  end

  test "has_many associations work through fixtures" do
    alice = users(:alice)
    assert alice.feedbacks.exists?
    assert_includes alice.feedbacks, feedbacks(:one)
  end
end
Use ERB for dynamic values and calculations

Fixture with ERB:

# test/fixtures/products.yml
tshirt:
  name: T-Shirt
  price: <%= 19.99 %>
  inventory_count: 15
  sku: <%= "TSH-#{SecureRandom.hex(4)}" %>
  created_at: <%= Time.current %>

shoes:
  name: Running Shoes
  price: <%= 89.99 %>
  inventory_count: 0
  on_sale: <%= true %>
  sale_price: <%= 89.99 * 0.8 %>  # 20% off
  created_at: <%= 3.months.ago %>

Testing Dynamic Values:

class ERBFixturesTest < ActiveSupport::TestCase
  test "ERB is evaluated in fixtures" do
    tshirt = products(:tshirt)
    assert_equal 19.99, tshirt.price
    assert tshirt.created_at
    assert tshirt.sku.present?
  end

  test "dynamic calculations work" do
    shoes = products(:shoes)
    assert shoes.on_sale?
    assert_in_delta 71.99, shoes.sale_price, 0.01
  end
end

Testing Jobs and Mailers

Test background job execution
class SendFeedbackJobTest < ActiveJob::TestCase
  test "enqueues job with correct arguments" do
    feedback = feedbacks(:one)

    assert_enqueued_with(job: SendFeedbackJob, args: [feedback]) do
      SendFeedbackJob.perform_later(feedback)
    end
  end

  test "performs job successfully" do
    feedback = feedbacks(:one)

    assert_difference "ActionMailer::Base.deliveries.size", 1 do
      SendFeedbackJob.perform_now(feedback)
    end

    assert_equal "delivered", feedback.reload.status
  end

  test "handles job failures gracefully" do
    feedback = feedbacks(:one)

    # Simulate external service failure
    EmailService.stub :send_feedback, -> (*) { raise StandardError.new("Service down") } do
      assert_raises(StandardError) do
        SendFeedbackJob.perform_now(feedback)
      end
    end

    # Status should not change on failure
    assert_equal "pending", feedback.reload.status
  end
end
Test email delivery and content
class FeedbackMailerTest < ActionMailer::TestCase
  test "notification email has correct content" do
    feedback = feedbacks(:one)
    email = FeedbackMailer.notification(feedback)

    assert_emails 1 do
      email.deliver_now
    end

    assert_equal ["noreply@example.com"], email.from
    assert_equal [feedback.recipient_email], email.to
    assert_equal "New Feedback Received", email.subject
    assert_match feedback.content, email.body.encoded
  end

  test "includes unsubscribe link" do
    feedback = feedbacks(:one)
    email = FeedbackMailer.notification(feedback)

    assert_match /unsubscribe/, email.body.encoded
  end

  test "uses correct email template" do
    feedback = feedbacks(:one)
    email = FeedbackMailer.notification(feedback)

    assert_match "feedback/notification", email.body.encoded
  end
end

Advanced Fixtures

Define fixtures with polymorphic associations

Fixtures:

# test/fixtures/comments.yml
feedback_comment:
  content: Great feedback!
  commentable: one (Feedback)  # Polymorphic association
  user: alice
  created_at: <%= 1.day.ago %>

article_comment:
  content: Interesting article
  commentable: first_article (Article)  # Different type
  user: bob
  created_at: <%= 2.days.ago %>

Testing:

class PolymorphicFixturesTest < ActiveSupport::TestCase
  test "polymorphic associations in fixtures" do
    feedback_comment = comments(:feedback_comment)
    article_comment = comments(:article_comment)

    assert_instance_of Feedback, feedback_comment.commentable
    assert_instance_of Article, article_comment.commentable
    assert_equal "Feedback", feedback_comment.commentable_type
  end
end
Share reusable logic across fixtures with helper methods

Define Helpers:

# test/test_helper.rb
module FixtureFileHelpers
  def default_avatar_url
    "https://example.com/default-avatar.png"
  end

  def formatted_date(date)
    date.strftime("%Y-%m-%d")
  end

  def default_password_digest
    BCrypt::Password.create("password123", cost: 4)
  end

  def admin_permissions
    %w[read write delete admin].to_json
  end
end

# Make helpers available to fixtures
ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers

Use in Fixtures:

# test/fixtures/users.yml
david:
  name: David
  email: david@example.com
  avatar_url: <%= default_avatar_url %>
  registered_on: <%= formatted_date(1.month.ago) %>
  password_digest: <%= default_password_digest %>

admin:
  name: Admin User
  email: admin@example.com
  permissions: <%= admin_permissions %>

Testing:

class FixtureHelpersTest < ActiveSupport::TestCase
  test "uses fixture helper methods" do
    david = users(:david)
    assert_equal "https://example.com/default-avatar.png", david.avatar_url
    assert BCrypt::Password.new(david.password_digest).is_password?("password123")
  end
end
Load only specific fixtures for test classes

Load All (Default):

# test/test_helper.rb
class ActiveSupport::TestCase
  fixtures :all  # Load all fixtures
  self.use_transactional_tests = true
end

Load Specific:

# test/models/feedback_test.rb
class FeedbackTest < ActiveSupport::TestCase
  fixtures :users, :feedbacks  # Only specific fixtures

  test "only users and feedbacks are loaded" do
    assert users(:alice)
    assert feedbacks(:one)
  end
end

Disable Fixtures:

# test/models/manual_test.rb
class ManualTest < ActiveSupport::TestCase
  self.use_instantiated_fixtures = false

  def setup
    @user = User.create!(name: "Manual User", email: "manual@example.com")
  end

  test "uses manually created data" do
    assert @user.persisted?
  end
end

Mocking and Stubbing

Replace a method temporarily with predetermined return value
class FeedbackTest < ActiveSupport::TestCase
  test "stubs instance method" do
    user = users(:alice)

    user.stub :name, "Stubbed Name" do
      assert_equal "Stubbed Name", user.name
    end

    assert_equal "Alice Johnson", user.name  # Restored after block
  end

  test "stubs with lambda for dynamic return" do
    feedback = feedbacks(:one)

    feedback.stub :content, -> { "Dynamic: #{Time.current}" } do
      assert_match /^Dynamic:/, feedback.content
    end
  end
end

Key Points:

  • Stub is scoped to the block
  • Original method restored automatically
  • Use lambda for dynamic return values
Create mock objects to verify method calls and arguments
class MinitestMockTest < ActiveSupport::TestCase
  test "creates mock object" do
    mock = Minitest::Mock.new
    mock.expect :call, "mocked result", ["arg1", "arg2"]

    result = mock.call("arg1", "arg2")

    assert_equal "mocked result", result
    mock.verify  # REQUIRED
  end

  test "uses assert_mock for auto-verification" do
    mock = Minitest::Mock.new
    mock.expect :call, "result"

    assert_mock mock do
      mock.call
    end  # Automatically calls verify
  end
end

Important: Always call mock.verify or use assert_mock to ensure expectations were met.

Stub HTTP requests with WebMock (REQUIRED per TEAM_RULES.md Rule #18)

Setup:

# Gemfile
gem "webmock", group: :test

# test/test_helper.rb
require "webmock/minitest"

Basic HTTP Stubs:

class WebMockTest < ActiveSupport::TestCase
  test "stubs HTTP GET request" do
    stub_request(:get, "https://api.example.com/feedback")
      .to_return(status: 200, body: '{"status":"success"}')

    response = Net::HTTP.get(URI("https://api.example.com/feedback"))
    assert_equal '{"status":"success"}', response
  end

  test "stubs POST with body matching" do
    stub_request(:post, "https://api.example.com/ai/improve")
      .with(body: hash_including(content: "Test feedback"))
      .to_return(status: 200, body: '{"improved":"Enhanced"}')
  end

  test "simulates timeout" do
    stub_request(:get, "https://api.example.com/slow").to_timeout

    assert_raises(Net::OpenTimeout) do
      Net::HTTP.get(URI("https://api.example.com/slow"))
    end
  end

  test "verifies HTTP request was made" do
    stub_request(:get, "https://api.example.com/check").to_return(status: 200)

    Net::HTTP.get(URI("https://api.example.com/check"))

    assert_requested :get, "https://api.example.com/check", times: 1
  end
end
Stub external API clients and third-party services
class ExternalDependenciesTest < ActiveSupport::TestCase
  test "stubs external API client" do
    AIService.stub :improve_content, "Improved content" do
      result = AIService.improve_content(feedbacks(:one).content)
      assert_equal "Improved content", result
    end
  end

  test "simulates external service error" do
    AIService.stub :improve_content, -> (*) { raise StandardError.new("API Error") } do
      assert_raises(StandardError) { AIService.improve_content("test") }
    end
  end
end
Design for testability with dependency injection

Bad - Hard to test:

# ❌ BAD
class FeedbackProcessorBad
  def process(feedback)
    improved = AIService.improve_content(feedback.content)
    feedback.update!(content: improved)
  end
end

Good - Dependency injection:

# ✅ GOOD
class FeedbackProcessorGood
  def initialize(ai_service: AIService)
    @ai_service = ai_service
  end

  def process(feedback)
    improved = @ai_service.improve_content(feedback.content)
    feedback.update!(content: improved)
  end
end

Test:

class DependencyInjectionTest < ActiveSupport::TestCase
  test "uses dependency injection instead of mocking" do
    fake_ai_service = Object.new
    def fake_ai_service.improve_content(content)
      "Improved: #{content}"
    end

    processor = FeedbackProcessorGood.new(ai_service: fake_ai_service)
    processor.process(feedbacks(:one))

    assert_match /^Improved:/, feedbacks(:one).content
  end
end

Custom Test Helpers

Configure test environment and include helper modules

test/test_helper.rb:

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

module ActiveSupport
  class TestCase
    parallelize(workers: :number_of_processors)
    fixtures :all

    # Include custom test helpers globally
    include TestHelpers::Authentication
    include TestHelpers::ApiHelpers
    include TestHelpers::AssertionHelpers
  end
end

Rails.logger.level = Logger::WARN
Simplify user authentication in controller and integration tests

test/test_helpers/authentication.rb:

module TestHelpers
  module Authentication
    def sign_in_as(user)
      post sign_in_url, params: { email: user.email, password: "password" }
    end

    def sign_out
      delete sign_out_url
    end

    def signed_in?
      session[:user_id].present?
    end

    def create_and_sign_in_user(**attrs)
      user = User.create!({ name: "Test", email: "test@example.com", password: "password" }.merge(attrs))
      sign_in_as(user)
      user
    end
  end
end

Usage:

class ProfileControllerTest < ActionDispatch::IntegrationTest
  test "shows profile when signed in" do
    sign_in_as users(:alice)
    get profile_url
    assert_response :success
  end
end
Streamline API testing with JSON parsing and authenticated requests

test/test_helpers/api_helpers.rb:

module TestHelpers
  module ApiHelpers
    def json_response
      JSON.parse(response.body)
    end

    def api_get(url, user: nil, **options)
      headers = options[:headers] || {}
      headers["Authorization"] = "Bearer #{user.api_token}" if user
      get url, headers: headers, **options
    end

    def api_post(url, params: {}, user: nil)
      headers = { "Content-Type" => "application/json" }
      headers["Authorization"] = "Bearer #{user.api_token}" if user
      post url, params: params.to_json, headers: headers
    end

    def assert_json_response(expected_keys)
      actual = json_response.keys.map(&:to_sym)
      expected_keys.each { |key| assert_includes actual, key.to_sym }
    end
  end
end

Usage:

test "returns JSON feedback list" do
  api_get api_feedbacks_url, user: users(:alice)
  assert_response :success
  assert_json_response [:feedbacks, :total, :page]
end
Domain-specific assertions for clearer test intent

test/test_helpers/assertion_helpers.rb:

module TestHelpers
  module AssertionHelpers
    def assert_visible(selector, text: nil)
      text ? assert_selector(selector, text: text, visible: true) : assert_selector(selector, visible: true)
    end

    def assert_hidden(selector)
      assert_no_selector selector, visible: true
    end

    def assert_flash(type, message)
      assert_equal message, flash[type]
    end

    def assert_validation_error(model, attribute, fragment)
      refute model.valid?
      assert_match /#{fragment}/i, model.errors[attribute].join(", ")
    end

    def assert_email_sent_to(email, subject: nil)
      emails = ActionMailer::Base.deliveries.select { |e| e.to.include?(email) }
      assert emails.any?, "No email sent to #{email}"
      assert emails.any? { |e| e.subject == subject }, "No email with subject '#{subject}'" if subject
    end
  end
end

Usage:

test "shows error for invalid feedback" do
  assert_validation_error Feedback.new(content: nil), :content, "can't be blank"
end

test "sends notification email" do
  FeedbackMailer.notification(feedbacks(:one)).deliver_now
  assert_email_sent_to "user@example.com", subject: "New Feedback"
end
Lightweight factory methods for creating test data

test/test_helpers/factory_helpers.rb:

module TestHelpers
  module FactoryHelpers
    def create_user(**attrs)
      User.create!({ name: "User #{SecureRandom.hex(4)}", email: "#{SecureRandom.hex(4)}@example.com" }.merge(attrs))
    end

    def create_feedback(**attrs)
      Feedback.create!({ content: "Test content with minimum fifty characters required", recipient_email: "user@example.com", status: "pending" }.merge(attrs))
    end

    def create_admin_user(**attrs)
      create_user(attrs.merge(admin: true))
    end
  end
end

Usage:

test "admin can delete feedback" do
  sign_in_as create_admin_user
  delete feedback_url(create_feedback)
  assert_response :redirect
end

Note: Prefer fixtures for most tests. Use factories for unique attributes.


Performance and Database Testing

Test N+1 queries and database performance
class FeedbackPerformanceTest < ActiveSupport::TestCase
  test "avoids N+1 queries when loading feedbacks with users" do
    10.times do |i|
      user = User.create!(name: "User #{i}", email: "user#{i}@example.com")
      Feedback.create!(content: "Feedback #{i} with minimum fifty characters required", recipient_email: "test@example.com", sender: user)
    end

    # Without includes - N+1 problem
    assert_queries(11) do  # 1 for feedbacks + 10 for users
      Feedback.limit(10).each { |f| f.sender.name }
    end

    # With includes - optimized
    assert_queries(2) do  # 1 for feedbacks + 1 for users
      Feedback.includes(:sender).limit(10).each { |f| f.sender.name }
    end
  end

  test "bulk operations are efficient" do
    # Efficient bulk insert
    assert_queries(1) do
      Feedback.insert_all([
        { content: "Bulk 1 with fifty characters", recipient_email: "test@example.com" },
        { content: "Bulk 2 with fifty characters", recipient_email: "test@example.com" }
      ])
    end
  end
end

Note: assert_queries is not built-in. Add to test_helper.rb:

def assert_queries(num = nil, &block)
  queries = []
  subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
    queries << payload[:sql] unless payload[:name] == "SCHEMA"
  end
  yield
  assert_equal num, queries.size if num
ensure
  ActiveSupport::Notifications.unsubscribe(subscriber)
end
Validate all fixtures are valid records
class FixtureValidationTest < ActiveSupport::TestCase
  test "all user fixtures are valid" do
    User.find_each do |user|
      assert user.valid?, "#{user.name} invalid: #{user.errors.full_messages.join(', ')}"
    end
  end

  test "all feedback fixtures are valid" do
    Feedback.find_each do |feedback|
      assert feedback.valid?, "Feedback #{feedback.id} invalid: #{feedback.errors.full_messages.join(', ')}"
    end
  end

  test "feedback fixtures have required associations" do
    Feedback.find_each do |feedback|
      assert feedback.sender.present?, "Feedback #{feedback.id} missing sender"
    end
  end

  test "fixture associations are set correctly" do
    feedback = feedbacks(:one)
    assert_equal users(:alice), feedback.sender
    assert_equal users(:alice).id, feedback.sender_id
  end
end

Test Isolation

Configure parallel test execution for faster test runs

test/test_helper.rb:

class ActiveSupport::TestCase
  parallelize(workers: :number_of_processors)

  parallelize_setup do |worker|
    # Rails handles database setup automatically
  end

  parallelize_teardown do |worker|
    FileUtils.rm_rf(Rails.root.join("tmp", "test_worker_#{worker}"))
  end
end

Disable for specific tests:

class FeedbackTest < ActiveSupport::TestCase
  parallelize(workers: 1)

  test "requires exclusive database access" do
    # ...
  end
end
Stub time-dependent code (prefer travel_to when possible)
class TimeStubbingTest < ActiveSupport::TestCase
  # ✅ PREFERRED: Use travel_to
  test "uses travel_to for time manipulation" do
    frozen_time = Time.zone.local(2024, 10, 29, 12, 0, 0)

    travel_to frozen_time do
      assert_equal frozen_time, Time.current
      assert_equal frozen_time.to_date, Date.today
    end
  end

  # Alternative: Stub when travel_to insufficient
  test "stubs Time.current" do
    Time.stub :current, Time.zone.local(2024, 10, 29, 12, 0, 0) do
      assert_equal Time.zone.local(2024, 10, 29, 12, 0, 0), Time.current
    end
  end
end

Recommendation: Always prefer travel_to over stubbing time. It's more comprehensive and handles edge cases better.


Anti-Patterns

Writing tests after writing code Defeats the purpose of TDD - tests should drive design
# ❌ BAD - Code written first, then tests
# ✅ GOOD - RED-GREEN-REFACTOR cycle
# 1. Write failing test
# 2. Write minimal code to pass
# 3. Refactor
Testing multiple concerns in one test Makes tests harder to debug when they fail
# ❌ BAD - Multiple validations in one test
test "feedback validations" do
  feedback = Feedback.new
  assert_not feedback.valid?
  assert_includes feedback.errors[:content], "can't be blank"
  assert_includes feedback.errors[:email], "can't be blank"
end
# ✅ GOOD - One concern per test
test "invalid without content" do
  feedback = Feedback.new(recipient_email: "user@example.com")
  assert_not feedback.valid?
  assert_includes feedback.errors[:content], "can't be blank"
end
Not using fixtures for test data Makes tests slower and harder to maintain
# ❌ BAD - Creating records in every test
test "feedback belongs to user" do
  user = User.create!(email: "test@example.com")
  feedback = Feedback.create!(content: "Test feedback with fifty characters", user: user)
  assert_equal user, feedback.user
end
# ✅ GOOD - Use fixtures
# test/fixtures/users.yml: alice: { email: alice@example.com }
# test/fixtures/feedbacks.yml: one: { content: "Great!", user: alice }

test "feedback belongs to user" do
  assert_equal users(:alice), feedbacks(:one).user
end
Forgetting to call mock.verify Mock expectations are not validated, test may pass incorrectly
# ❌ BAD - Expectations not verified
test "forgets to verify mock" do
  mock = Minitest::Mock.new
  mock.expect :call, "result"
  # NO mock.verify called
end
# ✅ GOOD - Always verify
test "verifies mock expectations" do
  mock = Minitest::Mock.new
  mock.expect :call, "result"

  mock.call
  mock.verify
end

# ✅ BETTER - Use assert_mock
test "uses assert_mock" do
  mock = Minitest::Mock.new
  mock.expect :call, "result"

  assert_mock mock do
    mock.call
  end
end
Not using WebMock for HTTP requests Violates TEAM_RULES.md Rule #18, makes tests slow and brittle
# ❌ BAD - Real HTTP request in test
test "makes real HTTP request" do
  response = Net::HTTP.get(URI("https://api.example.com/feedback"))
  assert_includes response, "success"
end
# ✅ GOOD - Use WebMock (REQUIRED)
test "stubs HTTP request with WebMock" do
  stub_request(:get, "https://api.example.com/feedback")
    .to_return(status: 200, body: '{"status":"success"}')

  response = Net::HTTP.get(URI("https://api.example.com/feedback"))
  assert_includes response, "success"
end
Hardcoding IDs in fixtures Brittle, causes test failures, defeats auto-generation
# ❌ BAD - Hardcoded IDs
alice:
  id: 1
  name: Alice Johnson
one:
  id: 100
  sender_id: 1  # ❌ Hardcoded FK
# ✅ GOOD - Let Rails generate IDs
alice:
  name: Alice Johnson
one:
  sender: alice  # ✅ Reference by name
Testing implementation details in helpers Couples tests to internal implementation
# ❌ BAD - Directly manipulates session
def sign_in_as(user)
  session[:user_id] = user.id
  session[:authenticated_at] = Time.current
  cookies.signed[:remember_token] = user.remember_token
end
# ✅ GOOD - Uses public interface
def sign_in_as(user)
  post sign_in_url, params: { email: user.email, password: "password" }
end

Running Tests

# Run all tests
rails test

# Run specific test file
rails test test/models/feedback_test.rb

# Run specific test by line number
rails test test/models/feedback_test.rb:12

# Run tests matching pattern
rails test -n /validation/

# Run in parallel (faster)
rails test --parallel

# Run all model tests
rails test test/models/

# Run system tests
rails test:system

- superpowers:test-driven-development - TDD process and discipline - rails-ai:models - Test model validations, associations, scopes - rails-ai:controllers - Test controller actions, routing - rails-ai:views - View and system testing patterns - rails-ai:hotwire - Test Turbo Streams, Stimulus controllers - rails-ai:security - Test security measures (XSS prevention, auth) - rails-ai:jobs - Test background jobs, SolidQueue

Official Documentation:

Gems & Libraries: