smithery/zerobearing2

rails-ai:testing

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

Installation

$ npx skills add smithery/zerobearing2 --skill rails-aitesting

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/zerobearing2 · top by installs.

npx skills add smithery/zerobearing2

Browse all from smithery/zerobearing2

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 48,606 B
  • docs SUMMARY.md 122 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Testing Rails Applications with Minitest

<superpowers-integration> 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 </superpowers-integration>

<when-to-use>

  • 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

</when-to-use>

<benefits>

  • 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

</benefits>

<team-rules-enforcement> 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

</team-rules-enforcement>

<verification-checklist> 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)

</verification-checklist>

<standards>

  • 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)

</standards>


TDD Red-Green-Refactor

<pattern name="red-green-refactor"> <description>Core TDD cycle - write failing test, make it pass, refactor</description>

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 </pattern>


Test Structure

<pattern name="basic-test-structure"> <description>Standard Minitest test class structure</description>

# 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

</pattern>

<pattern name="setup-and-teardown"> <description>Prepare and clean up test environment</description>

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

</pattern>


Minitest Assertions

<pattern name="common-assertions"> <description>Most frequently used Minitest assertions</description>

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: "[email protected]")
    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

</pattern>


Model Testing

Testing Validations

<pattern name="presence-validations"> <description>Test required fields are validated</description>

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: "[email protected]"
    )
    assert feedback.valid?
  end

  test "invalid without content" do
    feedback = Feedback.new(recipient_email: "[email protected]")
    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

</pattern>

<pattern name="format-validations"> <description>Test format validations like email, URL, phone number</description>

class FeedbackTest < ActiveSupport::TestCase
  test "invalid with malformed email" do
    invalid_emails = ["not-an-email", "@example.com", "user@", "user [email protected]"]

    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 = ["[email protected]", "[email protected]", "[email protected]"]

    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

</pattern>

<pattern name="length-validations"> <description>Test minimum and maximum length constraints</description>

class FeedbackTest < ActiveSupport::TestCase
  test "invalid with content below minimum length" do
    feedback = Feedback.new(content: "Too short", recipient_email: "[email protected]")
    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: "[email protected]").valid?
    assert Feedback.new(content: "a" * 5000, recipient_email: "[email protected]").valid?
  end

  test "invalid above maximum length" do
    feedback = Feedback.new(content: "a" * 5001, recipient_email: "[email protected]")
    assert_not feedback.valid?
    assert_includes feedback.errors[:content], "is too long (maximum is 5000 characters)"
  end
end

</pattern>

<pattern name="custom-validations"> <description>Test custom validation methods</description>

# 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: "[email protected]")
    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: "[email protected]")
    assert feedback.valid?
  end
end

</pattern>

Testing Associations

<pattern name="belongs-to-associations"> <description>Test belongs_to relationships and options</description>

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: "[email protected]", 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

</pattern>

<pattern name="has-many-associations"> <description>Test has_many relationships and dependent options</description>

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: "[email protected]") }

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

</pattern>

Testing Scopes

<pattern name="time-based-scopes"> <description>Test scopes with time conditions</description>

class FeedbackTest < ActiveSupport::TestCase
  test "recent scope returns feedbacks from last 30 days" do
    old = Feedback.create!(content: "Old fifty character feedback", recipient_email: "[email protected]", created_at: 31.days.ago)
    recent = Feedback.create!(content: "Recent fifty character feedback", recipient_email: "[email protected]", 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: "[email protected]", created_at: 31.days.ago)
    assert_empty Feedback.recent
  end
end

</pattern>

<pattern name="status-based-scopes"> <description>Test scopes filtering by status or state</description>

class FeedbackTest < ActiveSupport::TestCase
  test "unread scope returns only delivered feedbacks" do
    pending = Feedback.create!(content: "Pending fifty characters", recipient_email: "[email protected]", status: "pending")
    delivered = Feedback.create!(content: "Delivered fifty characters", recipient_email: "[email protected]", status: "delivered")
    read = Feedback.create!(content: "Read fifty characters", recipient_email: "[email protected]", status: "read")

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

</pattern>

Testing Callbacks

<pattern name="after-create-callbacks"> <description>Test callbacks that run after record creation</description>

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: "[email protected]")
    end
  end

  test "does not enqueue job when creation fails" do
    assert_no_enqueued_jobs do
      Feedback.new(content: nil).save
    end
  end
end

</pattern>

<pattern name="before-save-callbacks"> <description>Test callbacks that modify records before saving</description>

# 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: "[email protected]")
    assert_not_includes feedback.content, "<script>"
    assert_includes feedback.content, "Valid"
  end
end

</pattern>

Testing Instance Methods

<pattern name="state-transition-methods"> <description>Test methods that change record state</description>

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

</pattern>

Testing Enums

<pattern name="enum-states"> <description>Test enum definitions and state transitions</description>

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: "[email protected]", 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: "[email protected]", status: "pending")
    delivered = Feedback.create!(content: "Delivered fifty chars", recipient_email: "[email protected]", status: "delivered")

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

</pattern>

Testing Class Methods

<pattern name="class-method-queries"> <description>Test custom class methods that query records</description>

# 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: "[email protected]", status: "delivered", delivered_at: 10.days.ago)
    has_resp = Feedback.create!(content: "Has fifty chars", recipient_email: "[email protected]", status: "delivered", delivered_at: 10.days.ago)
    has_resp.create_response!(content: "Thank you")
    too_recent = Feedback.create!(content: "Recent fifty chars", recipient_email: "[email protected]", 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

</pattern>

<pattern name="class-method-calculations"> <description>Test class methods that perform calculations</description>

# 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: "[email protected]", created_at: 5.days.ago)
    f1.create_response!(content: "R1", created_at: 4.days.ago)
    f2 = Feedback.create!(content: "Second fifty chars", recipient_email: "[email protected]", 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: "[email protected]")
    assert_nil Feedback.average_response_time
  end
end

</pattern>

Testing Edge Cases

<pattern name="boundary-conditions"> <description>Test behavior at boundaries (min/max values, empty states)</description>

class FeedbackTest < ActiveSupport::TestCase
  test "handles empty collections gracefully" do
    feedback = Feedback.create!(content: "Feedback fifty chars", recipient_email: "[email protected]")
    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: "[email protected]", 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: "[email protected]")
    assert_equal unicode, feedback.reload.content
  end
end

</pattern>

<pattern name="error-handling"> <description>Test proper error handling and exception cases</description>

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

</pattern>


Controller and Integration Testing

<pattern name="controller-action-tests"> <description>Testing controller actions and responses</description>

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: "[email protected]" } }
    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

</pattern>


System Testing

<pattern name="system-test"> <description>Full-stack feature testing with browser simulation</description>

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: "[email protected]"
    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

</pattern>


Fixtures Design

<pattern name="basic-yaml-fixtures"> <description>Define simple fixture data in YAML format</description>

Fixture File:

# test/fixtures/users.yml
alice:
  name: Alice Johnson
  email: [email protected]
  active: true
  created_at: <%= 1.week.ago %>

bob:
  name: Bob Smith
  email: [email protected]
  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

</pattern>

<pattern name="association-fixtures"> <description>Define associations between fixtures using names</description>

Fixture Files:

# test/fixtures/users.yml
alice:
  name: Alice Johnson
  email: [email protected]

bob:
  name: Bob Smith
  email: [email protected]
# test/fixtures/feedbacks.yml
one:
  content: This is great feedback with minimum fifty characters!
  recipient_email: [email protected]
  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: [email protected]
  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 "[email protected]", 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

</pattern>

<pattern name="erb-dynamic-values"> <description>Use ERB for dynamic values and calculations</description>

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

</pattern>

Testing Jobs and Mailers

<pattern name="job-testing"> <description>Test background job execution</description>

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

</pattern>

<pattern name="mailer-testing"> <description>Test email delivery and content</description>

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 ["[email protected]"], 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

</pattern>


Advanced Fixtures

<pattern name="polymorphic-fixtures"> <description>Define fixtures with polymorphic associations</description>

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

</pattern>

<pattern name="fixture-helper-methods"> <description>Share reusable logic across fixtures with helper methods</description>

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: [email protected]
  avatar_url: <%= default_avatar_url %>
  registered_on: <%= formatted_date(1.month.ago) %>
  password_digest: <%= default_password_digest %>

admin:
  name: Admin User
  email: [email protected]
  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

</pattern>

<pattern name="selective-fixture-loading"> <description>Load only specific fixtures for test classes</description>

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: "[email protected]")
  end

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

</pattern>


Mocking and Stubbing

<pattern name="stub-instance-method"> <description>Replace a method temporarily with predetermined return value</description>

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

</pattern>

<pattern name="basic-mocking"> <description>Create mock objects to verify method calls and arguments</description>

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. </pattern>

<pattern name="webmock-http-stubs"> <description>Stub HTTP requests with WebMock (REQUIRED per TEAM_RULES.md Rule #18)</description>

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

</pattern>

<pattern name="stub-external-services"> <description>Stub external API clients and third-party services</description>

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

</pattern>

<pattern name="dependency-injection"> <description>Design for testability with dependency injection</description>

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

</pattern>


Custom Test Helpers

<pattern name="test-helper-setup"> <description>Configure test environment and include helper modules</description>

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

</pattern>

<pattern name="authentication-helpers"> <description>Simplify user authentication in controller and integration tests</description>

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: "[email protected]", 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

</pattern>

<pattern name="api-helpers"> <description>Streamline API testing with JSON parsing and authenticated requests</description>

test/testhelpers/apihelpers.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

</pattern>

<pattern name="assertion-helpers"> <description>Domain-specific assertions for clearer test intent</description>

test/testhelpers/assertionhelpers.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 "[email protected]", subject: "New Feedback"
end

</pattern>

<pattern name="factory-helpers"> <description>Lightweight factory methods for creating test data</description>

test/testhelpers/factoryhelpers.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: "[email protected]", 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. </pattern>


Performance and Database Testing

<pattern name="query-performance"> <description>Test N+1 queries and database performance</description>

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: "[email protected]", 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: "[email protected]" },
        { content: "Bulk 2 with fifty characters", recipient_email: "[email protected]" }
      ])
    end
  end
end

Note: assertqueries is not built-in. Add to testhelper.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

</pattern>

<pattern name="fixture-validation"> <description>Validate all fixtures are valid records</description>

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

</pattern>


Test Isolation

<pattern name="parallel-setup"> <description>Configure parallel test execution for faster test runs</description>

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

</pattern>

<pattern name="stub-time"> <description>Stub time-dependent code (prefer travel_to when possible)</description>

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. </pattern>


Anti-Patterns

<antipatterns> <antipattern> <description>Writing tests after writing code</description> <reason>Defeats the purpose of TDD - tests should drive design</reason> <bad-example>

# ❌ BAD - Code written first, then tests

</bad-example> <good-example>

# ✅ GOOD - RED-GREEN-REFACTOR cycle
# 1. Write failing test
# 2. Write minimal code to pass
# 3. Refactor

</good-example> </antipattern>

<antipattern> <description>Testing multiple concerns in one test</description> <reason>Makes tests harder to debug when they fail</reason> <bad-example>

# ❌ 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

</bad-example> <good-example>

# ✅ GOOD - One concern per test
test "invalid without content" do
  feedback = Feedback.new(recipient_email: "[email protected]")
  assert_not feedback.valid?
  assert_includes feedback.errors[:content], "can't be blank"
end

</good-example> </antipattern>

<antipattern> <description>Not using fixtures for test data</description> <reason>Makes tests slower and harder to maintain</reason> <bad-example>

# ❌ BAD - Creating records in every test
test "feedback belongs to user" do
  user = User.create!(email: "[email protected]")
  feedback = Feedback.create!(content: "Test feedback with fifty characters", user: user)
  assert_equal user, feedback.user
end

</bad-example> <good-example>

# ✅ GOOD - Use fixtures
# test/fixtures/users.yml: alice: { email: [email protected] }
# test/fixtures/feedbacks.yml: one: { content: "Great!", user: alice }

test "feedback belongs to user" do
  assert_equal users(:alice), feedbacks(:one).user
end

</good-example> </antipattern>

<antipattern> <description>Forgetting to call mock.verify</description> <reason>Mock expectations are not validated, test may pass incorrectly</reason> <bad-example>

# ❌ BAD - Expectations not verified
test "forgets to verify mock" do
  mock = Minitest::Mock.new
  mock.expect :call, "result"
  # NO mock.verify called
end

</bad-example> <good-example>

# ✅ 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

</good-example> </antipattern>

<antipattern> <description>Not using WebMock for HTTP requests</description> <reason>Violates TEAM_RULES.md Rule #18, makes tests slow and brittle</reason> <bad-example>

# ❌ 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

</bad-example> <good-example>

# ✅ 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

</good-example> </antipattern>

<antipattern> <description>Hardcoding IDs in fixtures</description> <reason>Brittle, causes test failures, defeats auto-generation</reason> <bad-example>

# ❌ BAD - Hardcoded IDs
alice:
  id: 1
  name: Alice Johnson
one:
  id: 100
  sender_id: 1  # ❌ Hardcoded FK

</bad-example> <good-example>

# ✅ GOOD - Let Rails generate IDs
alice:
  name: Alice Johnson
one:
  sender: alice  # ✅ Reference by name

</good-example> </antipattern>

<antipattern> <description>Testing implementation details in helpers</description> <reason>Couples tests to internal implementation</reason> <bad-example>

# ❌ 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

</bad-example> <good-example>

# ✅ GOOD - Uses public interface
def sign_in_as(user)
  post sign_in_url, params: { email: user.email, password: "password" }
end

</good-example> </antipattern> </antipatterns>


Running Tests

<testing>

# 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

</testing>


<related-skills>

  • 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

</related-skills>

<resources>

Official Documentation:

Gems & Libraries:

</resources>