minitest

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "minitest" with this command: npx skills add thinkoodle/rails-skills/thinkoodle-rails-skills-minitest

Rails Minitest Expert

Write performant, maintainable, and non-brittle tests for Rails applications using Minitest and fixtures.

Philosophy

Core Principles:

  1. Test behavior, not implementation - Focus on WHAT code does, not HOW
  2. Fast feedback loops - Prefer unit tests over integration tests, fixtures over factories
  3. Tests as documentation - Test names should describe expected behavior
  4. Minimal test data - Create only what's necessary; 2 records == many records
  5. 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 TypeLocationUse For
Modeltest/models/Validations, associations, business logic methods
Policytest/policies/Pundit authorization policies
Requesttest/requests/Full HTTP request/response cycle
Controllertest/controllers/Controller actions (prefer request tests)
Systemtest/system/Critical user flows with real browser
Servicetest/services/Service objects and complex operations
Jobtest/jobs/Background job behavior
Mailertest/mailers/Email content and delivery

Step 2: Check Existing Patterns

ALWAYS search for existing tests first:

# Find similar test files
rg "class.*Test < " test/

# Find existing fixtures
ls test/fixtures/

# Check for test helpers
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.

# AVOID - Factory Bot (slow, implicit)
let(:user) { create(:user) }
let(:project) { create(:project, workspace: workspace) }

# PREFER - Fixtures (fast, explicit)
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
# test/fixtures/users.yml
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  # Reference by fixture name

Step 4: Write Test Structure

Standard Test Structure:

require "test_helper"

class ModelTest < ActiveSupport::TestCase
  setup do
    # Load fixtures - MINIMAL setup only
    @record = models(:fixture_name)
  end

  # Group related tests with comments or test naming
  
  # Validation tests
  test "requires name" do
    @record.name = nil
    refute @record.valid?
    assert_includes @record.errors[:name], "can't be blank"
  end

  # Method tests
  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:

# SLOW - Creates database records
test "validates email format" do
  user = User.create!(email: "invalid", name: "Test")
  refute user.valid?
end

# FAST - Uses in-memory object
test "validates email format" do
  user = User.new(email: "invalid", name: "Test")
  refute user.valid?
end

Minimize Records Created:

# SLOW - Creates 25 records
test "paginates results" do
  create_list(:post, 25)
  # ...
end

# FAST - Configure pagination threshold for tests
# config/environments/test.rb: Pagy::DEFAULT[:limit] = 2
test "paginates results" do
  # Only need 3 records to test pagination with limit of 2
  assert_operator posts.count, :>=, 3
  # ...
end

Avoid Browser Tests When Possible:

# SLOW - Full browser simulation
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

# FAST - Request test (no browser)
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:

# BRITTLE - Exact timestamp match
assert_equal "2025-01-15T10:00:00Z", response["created_at"]

# ROBUST - Just verify presence
assert response["created_at"].present?

# BRITTLE - Exact error message
assert_equal "Name can't be blank", record.errors.full_messages.first

# ROBUST - Check for key content
assert_includes record.errors[:name], "can't be blank"

Use Inclusive Assertions:

# BRITTLE - Exact match
assert_equal({ id: 1, name: "Test", email: "test@example.com" }, response)

# ROBUST - Check key attributes only
assert_equal 1, response[:id]
assert_equal "Test", response[:name]
# OR
assert response.slice(:id, :name) == { id: 1, name: "Test" }

Step 7: Handle Multi-Tenancy

For acts_as_tenant projects, always wrap in tenant context:

# WRONG - Missing tenant context
test "admin can view project" do
  assert policy(@admin, @project).show?
end

# CORRECT - Proper tenant scoping
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
    # 1. Test denial WITHOUT permission
    refute policy(@member, Project).create?
    
    # 2. Grant permission
    set_workspace_permissions(@member, @workspace, :allowed_to_create_projects)
    
    # 3. Test allow WITH permission
    assert policy(@member, Project).create?
  end
end

Quick Reference

Assertion Mapping (RSpec to Minitest)

RSpecMinitest
expect(x).to eq(y)assert_equal y, x
expect(x).to be_truthyassert x
expect(x).to be_falseyrefute x
expect(x).to be_nilassert_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_validassert x.valid?
expect(x).not_to be_validrefute x.valid?
expect(x).to match(/pattern/)assert_match /pattern/, x

Rails-Specific Assertions

# Record changes
assert_difference "Post.count", 1 do
  Post.create!(title: "Test")
end

assert_no_difference "Post.count" do
  Post.new.save  # Invalid, doesn't save
end

# Value changes
assert_changes -> { post.reload.title }, from: "Old", to: "New" do
  post.update!(title: "New")
end

# Response assertions
assert_response :success
assert_response :redirect
assert_redirected_to post_path(post)

# DOM assertions
assert_select "h1", "Expected Title"
assert_select ".post", count: 3

# Query assertions
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:

  • Using fixtures instead of factories
  • Using User.new instead of User.create when DB not needed
  • Testing validation errors on in-memory objects
  • Minimal fixture data (only what's needed)
  • Request tests instead of system tests where possible
  • Pagination thresholds configured low for tests
  • No unnecessary associations in fixtures
  • BCrypt cost set to minimum in test environment
  • Logging disabled in test environment
  • Using build_stubbed pattern where applicable

Anti-Patterns to Avoid

  1. Testing implementation details - Test outcomes, not internal method calls
  2. Overly complex setup - If setup is > 10 lines, refactor to fixtures
  3. Shared state between tests - Each test should be independent
  4. Testing private methods - Only test public interface
  5. Brittle assertions - Don't assert on timestamps, exact errors, or order
  6. Too many system tests - Reserve for critical user paths only
  7. Missing negative tests - Always test what should fail/be denied
  8. Factory cascades - Avoid factories that create many associated records

Running Tests

# Run all tests
bin/rails test

# Run specific file
bin/rails test test/models/user_test.rb

# Run specific test by line
bin/rails test test/models/user_test.rb:42

# Run specific test by name
bin/rails test -n "test_requires_email"

# Run directory
bin/rails test test/policies/

# Run with verbose output
bin/rails test -v

# Run in parallel
bin/rails test --parallel

# Run with coverage
COVERAGE=true bin/rails test

# Run and fail fast
bin/rails test --fail-fast

Debugging Tips

# Print response body in request tests
puts response.body

# Print validation errors
pp @record.errors.full_messages

# Use breakpoint (Rails 7+)
debugger

# Check SQL queries
ActiveRecord::Base.logger = Logger.new(STDOUT)

# Inspect fixture data
pp users(:admin_user).attributes

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

rails-testing-rspec

No summary provided by upstream source.

Repository SourceNeeds Review
General

rails-active-storage

No summary provided by upstream source.

Repository SourceNeeds Review
Security

rails-security-audits

No summary provided by upstream source.

Repository SourceNeeds Review
General

rails-action-mailer

No summary provided by upstream source.

Repository SourceNeeds Review