Rails includes a comprehensive testing framework based on Minitest. Testing is baked into Rails from the start—every generated model, controller, and mailer includes a test file.
/plugin marketplace add sjnims/rails-expert/plugin install rails-expert@rails-expert-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/example-fixtures.ymlexamples/minitest-patterns.rbreferences/parallel-testing.mdreferences/tdd-workflow.mdreferences/test-types.mdRails includes a comprehensive testing framework based on Minitest. Testing is baked into Rails from the start—every generated model, controller, and mailer includes a test file.
Rails testing philosophy:
Rails provides several test types:
Rails uses Minitest by default. It's simple, fast, and built into Ruby.
Minitest:
test "product must have a name" do
product = Product.new(price: 9.99)
assert_not product.valid?
assert_includes product.errors[:name], "can't be blank"
end
RSpec (alternative):
it "must have a name" do
product = Product.new(price: 9.99)
expect(product).not_to be_valid
expect(product.errors[:name]).to include("can't be blank")
end
Rails philosophy: Use Minitest unless you have strong RSpec preference. Minitest is simpler, faster, and requires no extra gems.
test/
├── models/ # Model tests
├── controllers/ # Controller tests
├── integration/ # Integration tests
├── system/ # System tests (browser)
├── mailers/ # Mailer tests
├── jobs/ # Job tests
├── helpers/ # Helper tests
├── fixtures/ # Test data
└── test_helper.rb # Test configuration
require "test_helper"
class ProductTest < ActiveSupport::TestCase
test "should not save product without name" do
product = Product.new
assert_not product.save, "Saved product without name"
end
test "should save valid product" do
product = Product.new(name: "Widget", price: 9.99)
assert product.save, "Failed to save valid product"
end
end
Test data defined in YAML files.
# test/fixtures/products.yml
widget:
name: Widget
price: 9.99
available: true
category: electronics
gadget:
name: Gadget
price: 14.99
available: false
category: electronics
test "finds widget by name" do
widget = products(:widget) # Loads from fixtures
assert_equal "Widget", widget.name
assert_equal 9.99, widget.price
end
test "associates with category" do
widget = products(:widget)
assert_equal categories(:electronics), widget.category
end
# test/fixtures/products.yml
<% 10.times do |n| %>
product_<%= n %>:
name: <%= "Product #{n}" %>
price: <%= (n + 1) * 10 %>
<% end %>
# test/fixtures/categories.yml
electronics:
name: Electronics
# test/fixtures/products.yml
widget:
name: Widget
category: electronics # References category fixture
Test business logic, validations, associations, and instance methods.
require "test_helper"
class ProductTest < ActiveSupport::TestCase
test "requires name" do
product = Product.new(price: 9.99)
assert_not product.valid?
assert_includes product.errors[:name], "can't be blank"
end
test "requires positive price" do
product = Product.new(name: "Widget", price: -1)
assert_not product.valid?
assert_includes product.errors[:price], "must be greater than 0"
end
test "requires unique SKU" do
existing = products(:widget)
product = Product.new(name: "New", sku: existing.sku)
assert_not product.valid?
assert_includes product.errors[:sku], "has already been taken"
end
end
test "belongs to category" do
product = products(:widget)
assert_instance_of Category, product.category
end
test "has many reviews" do
product = products(:widget)
assert_respond_to product, :reviews
assert_kind_of ActiveRecord::Associations::CollectionProxy, product.reviews
end
test "calculates discount price" do
product = products(:widget)
product.discount_percentage = 10
assert_equal 8.99, product.discounted_price.round(2)
end
test "checks if in stock" do
product = products(:widget)
product.quantity = 5
assert product.in_stock?
product.quantity = 0
assert_not product.in_stock?
end
Test request handling, rendering, and redirects.
require "test_helper"
class ProductsControllerTest < ActionDispatch::IntegrationTest
test "should get index" do
get products_url
assert_response :success
assert_dom "h1", "Products"
end
test "should show product" do
get product_url(products(:widget))
assert_response :success
assert_dom "h2", "Widget"
end
test "should create product" do
assert_difference("Product.count", 1) do
post products_url, params: { product: { name: "New Widget", price: 9.99 } }
end
assert_redirected_to product_path(Product.last)
follow_redirect!
assert_response :success
end
test "should not create invalid product" do
assert_no_difference("Product.count") do
post products_url, params: { product: { price: 9.99 } } # Missing name
end
assert_response :unprocessable_entity
end
test "should update product" do
product = products(:widget)
patch product_url(product), params: { product: { name: "Updated" } }
assert_redirected_to product_path(product)
assert_equal "Updated", product.reload.name
end
test "should destroy product" do
product = products(:widget)
assert_difference("Product.count", -1) do
delete product_url(product)
end
assert_redirected_to products_path
end
end
Full browser simulation using Capybara.
require "application_system_test_case"
class ProductsTest < ApplicationSystemTestCase
test "creating a product" do
visit products_path
click_on "New Product"
fill_in "Name", with: "Widget"
fill_in "Price", with: "9.99"
select "Electronics", from: "Category"
click_on "Create Product"
assert_text "Product created successfully"
assert_text "Widget"
end
test "editing a product" do
product = products(:widget)
visit product_path(product)
click_on "Edit"
fill_in "Name", with: "Updated Widget"
click_on "Update Product"
assert_text "Product updated successfully"
assert_text "Updated Widget"
end
test "searching products" do
visit products_path
fill_in "Search", with: "Widget"
click_on "Search"
assert_text "Widget"
assert_no_text "Gadget"
end
end
assert true
assert_not false
assert_nil nil
assert_not_nil "value"
assert_empty []
assert_not_empty [1, 2, 3]
assert_equal 5, 2 + 3
assert_not_equal 5, 2 + 2
assert_match /widget/i, "Widget"
assert_no_match /foo/, "bar"
assert_includes [1, 2, 3], 2
assert_instance_of String, "hello"
assert_kind_of Numeric, 42
assert_respond_to product, :name
assert_raises(ActiveRecord::RecordInvalid) { product.save! }
assert_difference('Product.count', 1) { Product.create!(name: "Test") }
assert_no_difference('Product.count') { Product.new.save }
assert_changes -> { product.reload.price }, from: 9.99, to: 14.99
assert_no_changes -> { product.reload.price }
assert_response :success
assert_response :redirect
assert_redirected_to product_path(product)
assert_dom "h1", "Products"
assert_dom "div.product", count: 5
Test database query behavior:
# Assert exact query count
assert_queries_count(2) do
User.find(1)
User.find(2)
end
# Assert no queries (useful for caching tests)
assert_no_queries { cached_value }
# Match query patterns
assert_queries_match(/SELECT.*users/) { User.first }
assert_no_queries_match(/UPDATE/) { User.first }
Test error reporting behavior:
assert_error_reported(CustomError) do
Rails.error.report(CustomError.new("test"))
end
assert_no_error_reported do
safe_operation
end
Rails encourages TDD: write tests first, then implement.
Example:
# 1. RED - Write failing test
test "calculates discount price" do
product = Product.new(price: 100, discount_percentage: 10)
assert_equal 90, product.discounted_price
end
# Run test - FAILS (method doesn't exist)
# 2. GREEN - Minimal implementation
class Product < ApplicationRecord
def discounted_price
price - (price * discount_percentage / 100.0)
end
end
# Run test - PASSES
# 3. REFACTOR - Improve code
class Product < ApplicationRecord
def discounted_price
return price unless discount_percentage.present?
(price * (1 - discount_percentage / 100.0)).round(2)
end
end
# Run test - Still PASSES
See references/tdd-workflow.md for comprehensive TDD guidance.
# All tests
rails test
# Specific file
rails test test/models/product_test.rb
# Specific test
rails test test/models/product_test.rb:14
# By pattern
rails test test/models/*_test.rb
# Failed tests only
rails test --fail-fast
# Verbose output
rails test --verbose
Rails can run tests in parallel to speed up large test suites:
# test/test_helper.rb
class ActiveSupport::TestCase
parallelize(workers: :number_of_processors)
end
Run with custom worker count:
PARALLEL_WORKERS=4 rails test
Use threads instead of processes for lighter parallelization:
parallelize(workers: :number_of_processors, with: :threads)
See references/parallel-testing.md for comprehensive parallel testing guidance including setup/teardown hooks and debugging flaky tests.
Test time-sensitive code with ActiveSupport::Testing::TimeHelpers:
test "subscription expires after one year" do
user = users(:subscriber)
user.update!(subscribed_at: Time.current)
travel_to 1.year.from_now do
assert user.subscription_expired?
end
end
test "discount valid during sale period" do
travel_to Date.new(2024, 12, 25) do
assert Product.christmas_sale_active?
end
end
Available helpers:
travel_to(date_or_time) # Set current time within block
travel(duration) # Move time forward by duration
freeze_time # Freeze at current time
travel_back # Return to real time (automatic after block)
For deeper exploration:
references/tdd-workflow.md: Test-driven development in Railsreferences/test-types.md: Model, controller, integration, system test patternsreferences/parallel-testing.md: Parallel testing configuration and troubleshootingFor code examples:
examples/minitest-patterns.rb: Common testing patternsRails testing provides:
Master testing and you'll ship features with confidence.
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.