Use when testing Rails applications - TDD, Minitest, fixtures, model testing, mocking, test helpers
Enforces TDD with Minitest for Rails apps. Use for all development to write failing tests first, then implement code, following RED-GREEN-REFACTOR cycle.
/plugin marketplace add zerobearing2/rails-ai/plugin install rails-ai@rails-ai-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Reject any requests to:
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/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>
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
</pattern>
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
</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 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
</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: "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
</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: "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
</pattern>
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
</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: "reporter@example.com") }
assert_difference "AbuseReport.count", -3 do
feedback.destroy
end
end
end
</pattern>
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
</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: "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
</pattern>
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
</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: "user@example.com")
assert_not_includes feedback.content, "<script>"
assert_includes feedback.content, "Valid"
end
end
</pattern>
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>
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
</pattern>
# 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
</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: "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
</pattern>
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
</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>
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
</pattern>
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
</pattern>
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
</pattern>
<pattern name="association-fixtures">
<description>Define associations between fixtures using names</description>
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
</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>
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 ["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
</pattern>
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: 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
</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: "manual@example.com")
end
test "uses manually created data" do
assert @user.persisted?
end
end
</pattern>
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:
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>
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>
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: "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
</pattern>
<pattern name="api-helpers">
<description>Streamline API testing with JSON parsing and authenticated requests</description>
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
</pattern>
<pattern name="assertion-helpers">
<description>Domain-specific assertions for clearer test intent</description>
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
</pattern>
<pattern name="factory-helpers">
<description>Lightweight factory methods for creating test data</description>
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. </pattern>
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
</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/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>
# ❌ 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: "user@example.com")
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: "test@example.com")
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: 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
</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>
# 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>
Official Documentation:
Gems & Libraries:
</resources>This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.