Unnamed Skill
This skill should be used when the user asks about testing Rails applications, Minitest, test-driven development (TDD), unit tests, integration tests, system tests, fixtures, factories, mocking, stubbing, test coverage, continuous integration, test organization, or Rails testing best practices. Also use when discussing testing philosophy, test speed, or debugging failing tests. Examples:
$ 安裝
git clone https://github.com/sjnims/rails-expert /tmp/rails-expert && cp -r /tmp/rails-expert/plugins/rails-expert/skills/testing-minitest ~/.claude/skills/rails-expert// tip: Run this command in your terminal to install the skill
name: testing-minitest description: This skill should be used when the user asks about testing Rails applications, Minitest, test-driven development (TDD), unit tests, integration tests, system tests, fixtures, factories, mocking, stubbing, test coverage, continuous integration, test organization, or Rails testing best practices. Also use when discussing testing philosophy, test speed, or debugging failing tests. Examples:
Testing with Minitest: Rails Testing Philosophy
Overview
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.
Rails testing philosophy:
- Write tests early and often
- Test-driven development (TDD) is encouraged
- Tests are documentation
- Fast test suite enables confidence
- Test coverage prevents regressions
Rails provides several test types:
- Model tests: Business logic and validations
- Controller tests: Request handling
- Integration tests: Multi-controller workflows
- System tests: Full browser simulation
- Mailer tests: Email content and delivery
- Job tests: Background job behavior
Minitest vs RSpec
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 Structure
Test File Organization
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
Basic Test Structure
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
Fixtures
Test data defined in YAML files.
Defining Fixtures
# test/fixtures/products.yml
widget:
name: Widget
price: 9.99
available: true
category: electronics
gadget:
name: Gadget
price: 14.99
available: false
category: electronics
Using Fixtures
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
ERB in Fixtures
# test/fixtures/products.yml
<% 10.times do |n| %>
product_<%= n %>:
name: <%= "Product #{n}" %>
price: <%= (n + 1) * 10 %>
<% end %>
Associations in Fixtures
# test/fixtures/categories.yml
electronics:
name: Electronics
# test/fixtures/products.yml
widget:
name: Widget
category: electronics # References category fixture
Model Tests
Test business logic, validations, associations, and instance methods.
Validation Tests
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
Association Tests
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
Method Tests
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
Controller Tests
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
System Tests
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
Assertions
Basic Assertions
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! }
Rails-Specific Assertions
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
SQL Query Assertions
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 }
Error Reporter Assertions
Test error reporting behavior:
assert_error_reported(CustomError) do
Rails.error.report(CustomError.new("test"))
end
assert_no_error_reported do
safe_operation
end
Test-Driven Development (TDD)
Rails encourages TDD: write tests first, then implement.
TDD Workflow
- Red: Write failing test
- Green: Write minimal code to pass
- Refactor: Improve code while keeping tests green
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.
Running Tests
# 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
Parallel Testing
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.
Time Helpers
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)
Further Reading
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 troubleshooting
For code examples:
examples/minitest-patterns.rb: Common testing patterns
Summary
Rails testing provides:
- Minitest framework built into Rails
- Multiple test types for different layers
- Fixtures for test data
- System tests for browser simulation
- TDD workflow for confident development
- Fast test suite for rapid feedback
Master testing and you'll ship features with confidence.
Repository
