This skill should be used when the user asks about "Rails testing", "RSpec", "Minitest", "request specs", "system specs", "FactoryBot", "fixtures", "test coverage", "testing controllers", "testing models", "integration tests", or needs guidance on writing effective tests for Rails 7+ applications.
Detects your Rails testing framework (RSpec/Minitest) and provides production-ready test patterns for models, APIs, and system specs with proper setup, factories, and best practices.
/plugin marketplace add betamatt/claude-plugins/plugin install ruby-on-rails@betamatt-claude-pluginsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/testing-patterns.mdProduction-focused testing guidance supporting both RSpec and Minitest. Detect the project's testing framework and apply appropriate patterns.
Check for testing framework in use:
# RSpec if present
grep -q "rspec-rails" Gemfile && echo "RSpec"
# Check for spec directory
ls -d spec 2>/dev/null && echo "RSpec"
# Minitest (Rails default)
ls -d test 2>/dev/null && echo "Minitest"
spec/
├── factories/ # FactoryBot definitions
├── fixtures/files/ # File fixtures (images, PDFs)
├── models/ # Model specs
├── requests/ # Request specs (API testing)
├── services/ # Service object specs
├── system/ # System specs (browser testing)
├── support/ # Helpers, shared examples
│ ├── factory_bot.rb
│ ├── capybara.rb
│ └── shared_examples/
└── rails_helper.rb
# spec/requests/api/v1/orders_spec.rb
RSpec.describe "Orders API", type: :request do
let(:user) { create(:user) }
let(:headers) { { "Authorization" => "Bearer #{user.api_token}" } }
describe "GET /api/v1/orders" do
let!(:orders) { create_list(:order, 3, user: user) }
it "returns user orders" do
get "/api/v1/orders", headers: headers
expect(response).to have_http_status(:ok)
expect(json_response["orders"].size).to eq(3)
end
it "excludes other users orders" do
other_order = create(:order)
get "/api/v1/orders", headers: headers
expect(json_response["orders"].map { |o| o["id"] })
.not_to include(other_order.id)
end
end
describe "POST /api/v1/orders" do
let(:valid_params) do
{
order: {
shipping_address_id: create(:address, user: user).id,
line_items_attributes: [
{ product_id: create(:product).id, quantity: 2 }
]
}
}
end
it "creates an order" do
expect {
post "/api/v1/orders", params: valid_params, headers: headers
}.to change(Order, :count).by(1)
expect(response).to have_http_status(:created)
end
context "with invalid params" do
it "returns validation errors" do
post "/api/v1/orders",
params: { order: { line_items_attributes: [] } },
headers: headers
expect(response).to have_http_status(:unprocessable_entity)
expect(json_response["errors"]).to include(/line items/i)
end
end
end
private
def json_response
JSON.parse(response.body)
end
end
# spec/models/order_spec.rb
RSpec.describe Order, type: :model do
describe "validations" do
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:status) }
it "requires at least one line item" do
order = build(:order, line_items: [])
expect(order).not_to be_valid
expect(order.errors[:line_items]).to include("can't be empty")
end
end
describe "associations" do
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:line_items).dependent(:destroy) }
it { is_expected.to have_many(:products).through(:line_items) }
end
describe "scopes" do
describe ".pending" do
it "returns only pending orders" do
pending = create(:order, :pending)
completed = create(:order, :completed)
expect(Order.pending).to include(pending)
expect(Order.pending).not_to include(completed)
end
end
end
describe "#total" do
it "calculates sum of line item totals" do
order = create(:order)
create(:line_item, order: order, price: 10, quantity: 2)
create(:line_item, order: order, price: 5, quantity: 1)
expect(order.total).to eq(25)
end
end
end
# spec/system/checkout_spec.rb
RSpec.describe "Checkout", type: :system do
let(:user) { create(:user) }
let(:product) { create(:product, name: "Widget", price: 99) }
before do
sign_in user
create(:cart_item, user: user, product: product, quantity: 2)
end
it "completes checkout successfully" do
visit cart_path
expect(page).to have_content("Widget")
expect(page).to have_content("$198.00")
click_on "Proceed to Checkout"
fill_in "Street address", with: "123 Main St"
fill_in "City", with: "Portland"
select "Oregon", from: "State"
fill_in "Zip", with: "97201"
click_on "Place Order"
expect(page).to have_content("Order confirmed")
expect(page).to have_content("Order #")
end
it "shows validation errors for invalid address" do
visit checkout_path
click_on "Place Order"
expect(page).to have_content("Street address can't be blank")
end
end
# spec/factories/orders.rb
FactoryBot.define do
factory :order do
user
status { :pending }
transient do
items_count { 1 }
end
after(:build) do |order, evaluator|
if order.line_items.empty?
evaluator.items_count.times do
order.line_items << build(:line_item, order: order)
end
end
end
trait :pending do
status { :pending }
end
trait :completed do
status { :completed }
completed_at { Time.current }
end
trait :with_payment do
after(:create) do |order|
create(:payment, order: order)
end
end
end
end
# spec/support/shared_examples/authenticatable.rb
RSpec.shared_examples "requires authentication" do
context "without authentication" do
let(:headers) { {} }
it "returns unauthorized" do
make_request
expect(response).to have_http_status(:unauthorized)
end
end
end
# Usage
RSpec.describe "Orders API" do
describe "GET /api/v1/orders" do
it_behaves_like "requires authentication" do
let(:make_request) { get "/api/v1/orders", headers: headers }
end
end
end
test/
├── fixtures/ # YAML fixtures
├── controllers/ # Functional tests
├── integration/ # Integration tests
├── models/ # Unit tests
├── system/ # System tests
├── helpers/ # Helper tests
└── test_helper.rb
# test/models/order_test.rb
class OrderTest < ActiveSupport::TestCase
test "validates presence of user" do
order = Order.new(user: nil)
assert_not order.valid?
assert_includes order.errors[:user], "must exist"
end
test "calculates total correctly" do
order = orders(:pending_order)
assert_equal 150, order.total
end
test "scope pending returns only pending orders" do
pending_orders = Order.pending
assert pending_orders.all? { |o| o.status == "pending" }
end
end
# test/controllers/orders_controller_test.rb
class OrdersControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:john)
sign_in @user
end
test "should get index" do
get orders_url
assert_response :success
assert_select "h1", "Your Orders"
end
test "should create order" do
assert_difference("Order.count") do
post orders_url, params: {
order: {
shipping_address_id: addresses(:home).id,
line_items_attributes: [
{ product_id: products(:widget).id, quantity: 1 }
]
}
}
end
assert_redirected_to order_url(Order.last)
end
end
# test/system/checkouts_test.rb
class CheckoutsTest < ApplicationSystemTestCase
setup do
@user = users(:john)
sign_in @user
@cart = create_cart_with_items(@user)
end
test "completing checkout" do
visit cart_url
click_on "Checkout"
fill_in "Street address", with: "123 Main St"
fill_in "City", with: "Portland"
click_on "Place Order"
assert_text "Order confirmed"
end
end
# test/fixtures/orders.yml
pending_order:
user: john
status: pending
created_at: <%= 1.day.ago %>
completed_order:
user: john
status: completed
completed_at: <%= 1.hour.ago %>
# Use build instead of create when possible
order = build(:order) # No database hit
order = create(:order) # Database hit
# Use build_stubbed for even faster tests
order = build_stubbed(:order)
# Disable callbacks when not needed
user = create(:user, :skip_callbacks)
# spec/support/database_cleaner.rb
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, type: :system) do
DatabaseCleaner.strategy = :truncation
end
config.around(:each) do |example|
DatabaseCleaner.cleaning { example.run }
end
end
# spec/rails_helper.rb
RSpec.configure do |config|
config.before(:suite) do
# Prepare database for parallel testing
ActiveRecord::Base.connection.execute("SET SESSION lock_timeout = '2s'")
end
end
# Run tests in parallel
# PARALLEL_TEST_PROCESSORS=4 bundle exec rspec
For detailed patterns and examples:
references/testing-patterns.md - Advanced testing patterns, mocking, time testingreferences/ci-configuration.md - GitHub Actions, parallel testing setupThis skill should be used when the user asks to "add MCP server", "integrate MCP", "configure MCP in plugin", "use .mcp.json", "set up Model Context Protocol", "connect external service", mentions "${CLAUDE_PLUGIN_ROOT} with MCP", or discusses MCP server types (SSE, stdio, HTTP, WebSocket). Provides comprehensive guidance for integrating Model Context Protocol servers into Claude Code plugins for external tool and service integration.
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.
This skill should be used when the user asks about "plugin settings", "store plugin configuration", "user-configurable plugin", ".local.md files", "plugin state files", "read YAML frontmatter", "per-project plugin settings", or wants to make plugin behavior configurable. Documents the .claude/plugin-name.local.md pattern for storing plugin-specific configuration with YAML frontmatter and markdown content.