rails-ai:testing

Testing Rails Applications with Minitest

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rails-ai:testing" with this command: npx skills add zerobearing2/rails-ai/zerobearing2-rails-ai-rails-ai-testing

Testing Rails Applications with Minitest

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

TDD Red-Green-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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

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

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

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

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

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

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(&#x26;: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

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

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

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

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

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

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

❌ BAD - Code written first, then tests

✅ GOOD - RED-GREEN-REFACTOR cycle

1. Write failing test

2. Write minimal code to pass

3. Refactor

❌ 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

❌ 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

❌ 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

❌ 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

❌ 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

❌ 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

Official Documentation:

  • Rails Guides - Testing Rails Applications

  • Rails API - ActiveRecord::FixtureSet

  • Minitest Assertions

Gems & Libraries:

  • Minitest - Ruby testing framework

  • WebMock - HTTP request stubbing

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

rails-ai:hotwire

No summary provided by upstream source.

Repository SourceNeeds Review
General

rails-ai:mailers

No summary provided by upstream source.

Repository SourceNeeds Review
General

rails-ai:debugging-rails

No summary provided by upstream source.

Repository SourceNeeds Review