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.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/minitest-patterns.rbreferences/tdd-workflow.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_select "h1", "Products"
end
test "should show product" do
get product_url(products(:widget))
assert_response :success
assert_select "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_select "h1", "Products"
assert_select "div.product", count: 5
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
For deeper exploration:
references/tdd-workflow.md: Test-driven development in Railsreferences/test-types.md: Model, controller, integration, system test patternsFor code examples:
examples/minitest-patterns.rb: Common testing patternsRails testing provides:
Master testing and you'll ship features with confidence.