Agent skill
testing-minitest
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/testing/testing-minitest
SKILL.md
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.
Didn't find tool you were looking for?