| name | minitest |
| description | Expert guidance for writing fast, maintainable Minitest tests in Rails applications. Use when writing tests, converting from RSpec, debugging test failures, improving test performance, or following testing best practices. Covers model tests, policy tests, request tests, system tests, fixtures, and TDD workflows. |
| allowed-tools | Read, Grep, Glob, Write, Edit, Bash(bin/rails test:*), Bash(bin/rails test), Bash(bundle exec rails test:*) |
Rails Minitest Expert
Write performant, maintainable, and non-brittle tests for Rails applications using Minitest and fixtures.
Philosophy
Core Principles:
- Test behavior, not implementation - Focus on WHAT code does, not HOW
- Fast feedback loops - Prefer unit tests over integration tests, fixtures over factories
- Tests as documentation - Test names should describe expected behavior
- Minimal test data - Create only what's necessary; 2 records == many records
- Non-brittle assertions - Test outcomes, not exact values that may change
Testing Pyramid:
/\ System Tests (Few - critical paths only)
/ \
/____\ Request/Integration Tests (Some)
/ \
/________\ Unit Tests (Many - models, policies, services)
When To Use This Skill
- Writing new Minitest tests for Rails models, policies, controllers, or requests
- Converting RSpec tests to Minitest
- Debugging slow or flaky tests
- Improving test suite performance
- Following Rails testing conventions
- Writing fixture-based test data
- Implementing TDD workflows
Instructions
Step 1: Identify Test Type
Before writing, determine the appropriate test type:
| Test Type | Location | Use For |
|---|
| Model | test/models/ | Validations, associations, business logic methods |
| Policy | test/policies/ | Pundit authorization policies |
| Request | test/requests/ | Full HTTP request/response cycle |
| Controller | test/controllers/ | Controller actions (prefer request tests) |
| System | test/system/ | Critical user flows with real browser |
| Service | test/services/ | Service objects and complex operations |
| Job | test/jobs/ | Background job behavior |
| Mailer | test/mailers/ | Email content and delivery |
Step 2: Check Existing Patterns
ALWAYS search for existing tests first:
rg "class.*Test < " test/
ls test/fixtures/
cat test/test_helper.rb
cat test/support/*.rb
Match existing project conventions - consistency is more important than "best" patterns.
Step 3: Use Fixtures (Not Factories)
Fixtures are 10-100x faster than Factory Bot.
let(:user) { create(:user) }
let(:project) { create(:project, workspace: workspace) }
setup do
@workspace = workspaces(:main_workspace)
@user = users(:admin_user)
@project = projects(:active_project)
end
Fixture Best Practices:
- Create purpose-specific fixtures with descriptive names
- Use
<%= %> for dynamic values and UUIDs
- Reference associations by fixture name, not ID
- Keep fixtures minimal - only include required attributes
admin_user:
id: <%= Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "admin_user") %>
email: "admin@example.com"
name: "Admin User"
created_at: <%= 1.week.ago %>
member_user:
id: <%= Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "member_user") %>
email: "member@example.com"
name: "Member User"
workspace: main_workspace
Step 4: Write Test Structure
Standard Test Structure:
require "test_helper"
class ModelTest < ActiveSupport::TestCase
setup do
@record = models(:fixture_name)
end
test "requires name" do
@record.name = nil
refute @record.valid?
assert_includes @record.errors[:name], "can't be blank"
end
test "#full_name returns formatted name" do
@record.first_name = "John"
@record.last_name = "Doe"
assert_equal "John Doe", @record.full_name
end
end
Step 5: Follow Performance Guidelines
Avoid Database When Possible:
test "validates email format" do
user = User.create!(email: "invalid", name: "Test")
refute user.valid?
end
test "validates email format" do
user = User.new(email: "invalid", name: "Test")
refute user.valid?
end
Minimize Records Created:
test "paginates results" do
create_list(:post, 25)
end
test "paginates results" do
assert_operator posts.count, :>=, 3
end
Avoid Browser Tests When Possible:
class PostsSystemTest < ApplicationSystemTestCase
test "creates a post" do
visit new_post_path
fill_in "Title", with: "Test"
click_on "Create"
assert_text "Post created"
end
end
class PostsRequestTest < ActionDispatch::IntegrationTest
test "creates a post" do
post posts_path, params: { post: { title: "Test" } }
assert_response :redirect
follow_redirect!
assert_response :success
end
end
Step 6: Write Non-Brittle Assertions
Test Behavior, Not Exact Values:
assert_equal "2025-01-15T10:00:00Z", response["created_at"]
assert response["created_at"].present?
assert_equal "Name can't be blank", record.errors.full_messages.first
assert_includes record.errors[:name], "can't be blank"
Use Inclusive Assertions:
assert_equal({ id: 1, name: "Test", email: "test@example.com" }, response)
assert_equal 1, response[:id]
assert_equal "Test", response[:name]
assert response.slice(:id, :name) == { id: 1, name: "Test" }
Step 7: Handle Multi-Tenancy
For acts_as_tenant projects, always wrap in tenant context:
test "admin can view project" do
assert policy(@admin, @project).show?
end
test "admin can view project" do
with_workspace(@workspace) do
assert policy(@admin, @project).show?
end
end
Step 8: Test Permission Flows Correctly
Always test denial BEFORE granting, then allow AFTER:
test "member requires permission to create" do
with_workspace(@workspace) do
refute policy(@member, Project).create?
set_workspace_permissions(@member, @workspace, :allowed_to_create_projects)
assert policy(@member, Project).create?
end
end
Quick Reference
Assertion Mapping (RSpec to Minitest)
| RSpec | Minitest |
|---|
expect(x).to eq(y) | assert_equal y, x |
expect(x).to be_truthy | assert x |
expect(x).to be_falsey | refute x |
expect(x).to be_nil | assert_nil x |
expect(arr).to include(x) | assert_includes arr, x |
expect(arr).not_to include(x) | refute_includes arr, x |
expect { }.to change { X.count }.by(1) | assert_difference "X.count", 1 do ... end |
expect { }.to raise_error(E) | assert_raises(E) { ... } |
expect(x).to be_valid | assert x.valid? |
expect(x).not_to be_valid | refute x.valid? |
expect(x).to match(/pattern/) | assert_match /pattern/, x |
Rails-Specific Assertions
assert_difference "Post.count", 1 do
Post.create!(title: "Test")
end
assert_no_difference "Post.count" do
Post.new.save
end
assert_changes -> { post.reload.title }, from: "Old", to: "New" do
post.update!(title: "New")
end
assert_response :success
assert_response :redirect
assert_redirected_to post_path(post)
assert_select "h1", "Expected Title"
assert_select ".post", count: 3
assert_queries_count(2) { User.find(1); User.find(2) }
assert_no_queries { cached_value }
Test File Templates
Model Test:
require "test_helper"
class UserTest < ActiveSupport::TestCase
setup do
@user = users(:active_user)
end
test "valid fixture" do
assert @user.valid?
end
test "requires email" do
@user.email = nil
refute @user.valid?
assert_includes @user.errors[:email], "can't be blank"
end
test "#display_name returns formatted name" do
@user.name = "John Doe"
assert_equal "John Doe", @user.display_name
end
end
Request Test:
require "test_helper"
class PostsRequestTest < ActionDispatch::IntegrationTest
setup do
@user = users(:active_user)
@post = posts(:published_post)
sign_in @user
end
test "GET /posts returns success" do
get posts_path
assert_response :success
end
test "POST /posts creates record" do
assert_difference "Post.count", 1 do
post posts_path, params: { post: { title: "New Post", body: "Content" } }
end
assert_redirected_to post_path(Post.last)
end
test "POST /posts with invalid data returns error" do
assert_no_difference "Post.count" do
post posts_path, params: { post: { title: "" } }
end
assert_response :unprocessable_entity
end
end
Policy Test:
require "test_helper"
class PostPolicyTest < ActiveSupport::TestCase
include PolicyTestHelpers
setup do
@workspace = workspaces(:main_workspace)
@admin = users(:admin_user)
@member = users(:member_user)
@post = posts(:workspace_post)
Current.user = nil
end
test "admin can always edit" do
with_workspace(@workspace) do
assert policy(@admin, @post).edit?
end
end
test "member requires permission to edit" do
with_workspace(@workspace) do
refute policy(@member, @post).edit?
set_workspace_permissions(@member, @workspace, :allowed_to_edit_posts)
assert policy(@member, @post).edit?
end
end
test "scope excludes other workspace posts" do
with_workspace(@other_workspace) do
scope = PostPolicy::Scope.new(@admin, Post.all).resolve
refute_includes scope, @post
end
end
end
Performance Optimization Checklist
Before submitting tests, verify:
Anti-Patterns to Avoid
- Testing implementation details - Test outcomes, not internal method calls
- Overly complex setup - If setup is > 10 lines, refactor to fixtures
- Shared state between tests - Each test should be independent
- Testing private methods - Only test public interface
- Brittle assertions - Don't assert on timestamps, exact errors, or order
- Too many system tests - Reserve for critical user paths only
- Missing negative tests - Always test what should fail/be denied
- Factory cascades - Avoid factories that create many associated records
Running Tests
bin/rails test
bin/rails test test/models/user_test.rb
bin/rails test test/models/user_test.rb:42
bin/rails test -n "test_requires_email"
bin/rails test test/policies/
bin/rails test -v
bin/rails test --parallel
COVERAGE=true bin/rails test
bin/rails test --fail-fast
Debugging Tips
puts response.body
pp @record.errors.full_messages
debugger
ActiveRecord::Base.logger = Logger.new(STDOUT)
pp users(:admin_user).attributes