← Back to DevBytes

Minitest: Complete Testing Guide for Developers

What is Minitest?

Minitest is Ruby's built-in testing framework that ships with the standard library. It provides a lightweight, fast, and complete toolkit for writing unit tests, specifications, and even benchmarking code. Unlike heavier frameworks that require extensive configuration, Minitest works out of the box with zero dependencies beyond Ruby itself.

Minitest offers two distinct testing styles:

Both styles use the same underlying engine, so you can mix and match or choose the approach that fits your team's workflow best. The framework also includes built-in mocking with Minitest::Mock, test coverage reporting, and a plugin system for extending functionality.

Why Minitest Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

In a landscape crowded with testing tools, Minitest stands out for several compelling reasons that directly impact development velocity and code quality:

Speed and Simplicity

Minitest is intentionally minimal. The entire framework clocks in at roughly 3,000 lines of Ruby, making it blazingly fast to load and execute. There's no DSL to learn if you stick with unit-style assertions — just plain Ruby method calls. This minimalism translates to tests that run orders of magnitude faster than heavyweight alternatives, keeping your TDD feedback loop tight and productive.

Always Available

Because Minitest is part of Ruby's standard library, it's present in every Ruby installation. No Gemfile entries, no version conflicts with other gems, no installation hurdles on CI servers or deployment environments. You can write and run tests on any machine that has Ruby installed — period.

Full Test Toolkit

Despite its small footprint, Minitest packs everything you need: assertions, expectations, mocking, benchmarking, test organization, random test ordering (to surface hidden dependencies), and detailed failure reports. It's not a stripped-down toy — it's a carefully curated set of testing primitives that cover real-world needs without bloat.

Ruby Community Standard

Minitest is the default testing framework for Rails applications and is used extensively across the Ruby ecosystem. Understanding Minitest means you can jump into virtually any Ruby codebase and immediately understand its test suite. Many high-profile gems and applications rely on Minitest exclusively.

Getting Started: Installation and Setup

Since Minitest ships with Ruby, no installation is required for basic use. However, if you want the latest version or additional features, add it to your Gemfile:

# Gemfile
gem 'minitest'

# Or for a specific version
gem 'minitest', '~> 5.20'

To use Minitest in a standalone Ruby project, create a test file and require the framework at the top:

# test_calculator.rb
require 'minitest/autorun'

class Calculator
  def add(a, b)
    a + b
  end

  def subtract(a, b)
    a - b
  end

  def multiply(a, b)
    a * b
  end

  def divide(a, b)
    raise ArgumentError, "Cannot divide by zero" if b == 0
    a.to_f / b
  end
end

class TestCalculator < Minitest::Test
  def setup
    @calculator = Calculator.new
  end

  def test_add
    assert_equal 5, @calculator.add(2, 3)
  end

  def test_subtract
    assert_equal 1, @calculator.subtract(3, 2)
  end

  def test_multiply
    assert_equal 12, @calculator.multiply(3, 4)
  end

  def test_divide
    assert_equal 2.5, @calculator.divide(5, 2)
  end

  def test_divide_by_zero_raises_error
    assert_raises(ArgumentError) do
      @calculator.divide(5, 0)
    end
  end
end

Run the test file directly with Ruby:

$ ruby test_calculator.rb
Run options: --seed 12345

# Running:

.....
Finished in 0.001234s

5 runs, 5 assertions, 0 failures, 0 errors, 0 skips

Unit-Style Testing: Assertions Deep Dive

The unit-style approach uses test classes that inherit from Minitest::Test and methods prefixed with test_. Minitest provides a rich set of assertion methods that cover virtually every verification scenario you'll encounter:

Core Assertions

require 'minitest/autorun'

class TestAssertions < Minitest::Test
  # Equality assertions
  def test_equality
    assert_equal "hello", "hello"
    assert_equal 42, 42
    # For objects with custom == method
    assert_equal [1, 2, 3], [1, 2, 3]
  end

  def test_same_object_identity
    a = "hello"
    b = a
    assert_same a, b  # passes: same object
    refute_same "hello", "hello"  # passes: different string objects
  end

  # Boolean assertions
  def test_boolean
    assert true
    assert false  # this will fail!
    refute false  # passes
    assert_equal true, 5 > 3
  end

  # Nil assertions
  def test_nil
    assert_nil nil
    assert_nil @undefined_variable  # nil by default
  end

  # Truthy/falsy (checks boolean coercion)
  def test_truthiness
    assert_silent do
      # assert_true and assert_false are not the same as assert/refute
      # assert expects truthy (anything but nil/false)
    end
  end

  # Pattern matching
  def test_pattern_matching
    assert_match /world/, "hello world"
    assert_match /\d+/, "abc123def"
    refute_match /\d/, "abc"
  end

  # Type assertions
  def test_types
    assert_instance_of String, "hello"
    assert_kind_of Numeric, 42  # checks inheritance chain
    assert_kind_of Numeric, 42.5
    assert_kind_of Object, "hello"  # everything is an Object
  end

  # Inclusion assertions
  def test_inclusion
    assert_includes [1, 2, 3], 2
    assert_includes "hello world", "world"
    assert_includes({a: 1, b: 2}, :a)  # hash key check
  end
end

Exception and Error Assertions

class TestExceptionHandling < Minitest::Test
  def test_raises_specific_error
    assert_raises(ArgumentError) do
      raise ArgumentError, "Invalid input"
    end
  end

  def test_raises_with_message_matching
    exception = assert_raises(ArgumentError) do
      raise ArgumentError, "Invalid input: must be positive"
    end
    assert_match /positive/, exception.message
  end

  def test_multiple_exception_types
    assert_raises(ArgumentError, TypeError) do
      # passes if either is raised
      raise TypeError, "Wrong type"
    end
  end

  def test_silence_output
    assert_silent do
      # no output to STDOUT or STDERR
      1 + 1
    end
  end

  def test_output_capture
    assert_output("hello\n") do
      puts "hello"
    end

    assert_output(/hello/) do
      puts "hello world"
    end
  end
end

Numeric and Comparison Assertions

class TestNumericAssertions < Minitest::Test
  def test_delta_comparison
    # Assert two floats are equal within a delta
    assert_in_delta 0.05, 0.051, 0.01
    # This passes: |0.05 - 0.051| = 0.001 <= 0.01
  end

  def test_epsilon_comparison
    # Assert two values are equal within a relative epsilon
    assert_in_epsilon 1000, 1001, 0.01
    # Passes: relative difference is 0.001 < 0.01
  end

  def test_comparison_operators
    assert_operator 5, :>, 3
    assert_operator 10, :>=, 10
    assert_operator "abc", :<, "xyz"  # works with Comparable
    refute_operator 3, :>, 5
  end

  def test_respond_to
    assert_respond_to "hello", :upcase
    assert_respond_to [1, 2, 3], :map
    refute_respond_to 42, :upcase
  end
end

Collection and Hash Assertions

class TestCollectionAssertions < Minitest::Test
  def test_empty_collections
    assert_empty []
    assert_empty ""
    assert_empty({})
  end

  def test_hash_key_value
    h = { name: "Alice", age: 30, city: "NYC" }
    assert_includes h.keys, :name
    assert_includes h.values, "Alice"
    refute_includes h.keys, :unknown_key
  end

  def test_array_equality_ignoring_order
    # No built-in assertion for this, compose manually
    sorted_expected = [1, 2, 3].sort
    sorted_actual   = [3, 1, 2].sort
    assert_equal sorted_expected, sorted_actual
  end
end

Spec-Style Testing: BDD Syntax

For teams that prefer behavior-driven development syntax, Minitest provides a spec DSL through Minitest::Spec. This style uses describe blocks to group related behaviors and it blocks to specify individual expectations:

require 'minitest/autorun'
require 'minitest/spec'

# Override the default test class to use spec style
Minitest::Test.make_my_diffs_pretty!

describe Calculator do
  # Create a fresh calculator for each test
  before do
    @calculator = Calculator.new
  end

  describe "#add" do
    it "adds two positive numbers correctly" do
      expect(@calculator.add(2, 3)).must_equal 5
    end

    it "adds negative numbers correctly" do
      expect(@calculator.add(-2, -3)).must_equal -5
    end

    it "adds with zero correctly" do
      expect(@calculator.add(5, 0)).must_equal 5
    end
  end

  describe "#subtract" do
    it "subtracts two numbers" do
      expect(@calculator.subtract(10, 3)).must_equal 7
    end

    it "results in negative when subtracting larger from smaller" do
      expect(@calculator.subtract(3, 10)).must_equal -7
    end
  end

  describe "#divide" do
    it "divides two numbers evenly" do
      expect(@calculator.divide(10, 2)).must_equal 5.0
    end

    it "handles division resulting in float" do
      expect(@calculator.divide(5, 2)).must_equal 2.5
    end

    it "raises error on division by zero" do
      expect(-> { @calculator.divide(5, 0) }).must_raise ArgumentError
    end
  end
end

Expectation Matchers Reference

The spec style provides expectation matchers that mirror assertions. Every assertion has a corresponding must_ and wont_ expectation method:

describe "Expectation Matchers" do
  it "demonstrates common matchers" do
    # Equality
    expect(42).must_equal 42
    expect("hello").wont_equal "world"

    # Identity
    obj = Object.new
    expect(obj).must_be_same_as obj
    expect(Object.new).wont_be_same_as Object.new

    # Nil
    expect(nil).must_be_nil
    expect(42).wont_be_nil

    # Boolean
    expect(true).must_equal true
    expect(5 > 3).must_equal true

    # Pattern matching
    expect("hello world").must_match /world/
    expect("abc").wont_match /\d/

    # Types
    expect("hello").must_be_instance_of String
    expect(42).must_be_kind_of Numeric

    # Collections
    expect([1, 2, 3]).must_include 2
    expect([]).must_be_empty
    expect("").must_be_empty

    # Numeric
    expect(0.051).must_be_close_to 0.05, 0.01

    # Operators
    expect(5).must_be :>, 3
    expect(10).must_be :>=, 10

    # Respond to
    expect("hello").must_respond_to :upcase
  end

  it "demonstrates exception expectations" do
    expect(-> { raise ArgumentError }).must_raise ArgumentError
    expect(-> { 1 + 1 }).wont_raise
  end

  it "demonstrates output expectations" do
    expect { puts "hello" }.must_output("hello\n")
    expect { puts "test" }.must_output(/test/)
  end
end

Test Organization and Lifecycle Hooks

Minitest provides lifecycle hooks that let you set up preconditions and clean up after tests. Understanding the execution order is critical for writing reliable tests:

Setup and Teardown

require 'minitest/autorun'

class TestDatabaseOperations < Minitest::Test
  # Runs before each test method
  def setup
    @db = DatabaseConnection.new
    @db.connect
    @db.begin_transaction
    @user = @db.insert(:users, name: "Alice")
  end

  # Runs after each test method
  def teardown
    @db.rollback_transaction
    @db.disconnect
  end

  def test_find_user
    found = @db.find(:users, @user.id)
    assert_equal "Alice", found.name
  end

  def test_update_user
    @db.update(:users, @user.id, name: "Bob")
    updated = @db.find(:users, @user.id)
    assert_equal "Bob", updated.name
  end
end

# Spec-style hooks
describe DatabaseOperations do
  before do
    @db = DatabaseConnection.new
    @db.connect
  end

  after do
    @db.disconnect
  end

  it "connects successfully" do
    expect(@db).must_be :connected?
  end
end

Global Setup with before_run and after_run

class TestWithGlobalSetup < Minitest::Test
  # Class-level hooks (run once for the entire test class)
  def self.before_run
    @@shared_resource = ExpensiveSetup.create
  end

  def self.after_run
    @@shared_resource.cleanup
  end

  def test_uses_shared_resource
    assert @@shared_resource.available?
  end
end

Test Method Discovery

Minitest automatically discovers test methods by looking for methods whose names begin with test_ (unit style) or blocks passed to it (spec style). You can also use the test macro for inline test definitions:

class TestUsingMacro < Minitest::Test
  # The test macro creates a test method with a descriptive name
  test "authenticated user can access dashboard" do
    user = User.new(authenticated: true)
    assert user.can_access_dashboard?
  end

  test "unauthenticated user is redirected" do
    user = User.new(authenticated: false)
    refute user.can_access_dashboard?
  end
end

Mocking and Stubbing with Minitest::Mock

Minitest includes a built-in mocking library that lets you create test doubles, stub method calls, and verify interactions. This is essential for isolating units of code from their dependencies:

Basic Mocking

require 'minitest/autorun'

class TestOrderProcessor < Minitest::Test
  def test_process_order_sends_email
    # Create a mock email service
    mock_emailer = Minitest::Mock.new

    # Set expectations: method name, arguments, return value
    mock_emailer.expect :send_confirmation, true, ["alice@example.com"]
    mock_emailer.expect :send_receipt, true, ["alice@example.com", 12345]

    # Inject the mock into the system under test
    processor = OrderProcessor.new(email_service: mock_emailer)
    result = processor.process(order_id: 12345, email: "alice@example.com")

    assert result.success?

    # Verify all expected methods were called
    mock_emailer.verify
  end
end

Stubbing Methods

class TestWeatherReport < Minitest::Test
  def test_report_includes_temperature
    weather_api = Object.new

    # Stub the fetch_temperature method to return a fixed value
    weather_api.define_singleton_method(:fetch_temperature) do |city|
      { city: city, temp_c: 22 }
    end

    report = WeatherReport.new(api: weather_api)
    result = report.generate("London")

    assert_includes result, "22°C"
  end

  # Using Minitest stub method (temporary override)
  def test_temporary_stub
    calculator = Calculator.new

    # Temporarily stub add to return a fixed value
    calculator.stub :add, 100 do
      # Inside the block, add always returns 100
      assert_equal 100, calculator.add(2, 3)
      assert_equal 100, calculator.add(10, 20)
    end

    # After the block, original behavior is restored
    assert_equal 5, calculator.add(2, 3)
  end

  def test_stubbing_time
    # Freeze time for deterministic tests
    frozen_time = Time.new(2024, 1, 1, 12, 0, 0)

    Time.stub :now, frozen_time do
      assert_equal frozen_time, Time.now
      # Code that depends on Time.now will see the frozen value
    end
  end
end

Mock Expectation Patterns

class TestAdvancedMocking < Minitest::Test
  def test_mock_with_multiple_calls
    payment_gateway = Minitest::Mock.new

    # Expect charge to be called twice with different arguments
    payment_gateway.expect :charge, true, ["card_123", 1000]  # $10.00
    payment_gateway.expect :charge, true, ["card_456", 500]   # $5.00

    processor = PaymentProcessor.new(gateway: payment_gateway)
    processor.process_batch([
      { card_id: "card_123", amount: 1000 },
      { card_id: "card_456", amount: 500 }
    ])

    payment_gateway.verify
  end

  def test_mock_raises_on_unexpected_calls
    mock = Minitest::Mock.new
    mock.expect :allowed_method, true

    # Calling an unexpected method raises MockExpectationError
    assert_raises(Minitest::Mock::MockExpectationError) do
      mock.unexpected_method
    end
  end
end

Running Tests and Configuring Output

Minitest offers flexible test execution with various command-line options and output formats:

Running Specific Tests

# Run all tests in a directory
$ ruby -Ilib:test test/**/*_test.rb

# Run a single test file
$ ruby test/calculator_test.rb

# Run a specific test method by name (use --name flag)
$ ruby test/calculator_test.rb --name test_add

# Run tests matching a pattern
$ ruby test/calculator_test.rb --name /division/

# Run with specific seed for deterministic ordering
$ ruby test/calculator_test.rb --seed 12345

# Run in verbose mode
$ ruby test/calculator_test.rb --verbose

Customizing Output with Reporters

# Add minitest-reporters gem to Gemfile
# gem 'minitest-reporters'

require 'minitest/autorun'
require 'minitest/reporters'

# Use the spec reporter for RSpec-style output
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new

# Or use progress reporter with detailed failures
Minitest::Reporters.use! [
  Minitest::Reporters::ProgressReporter.new,
  Minitest::Reporters::DefaultReporter.new(detailed_skip: false)
]

class TestWithReporter < Minitest::Test
  def test_example
    assert_equal 4, 2 + 2
  end
end

Test Execution Options in Code

# Control test execution programmatically
require 'minitest/autorun'

class TestConfiguration < Minitest::Test
  # Control parallel execution (requires minitest 5.15+)
  # Minitest.parallel_executor = :io_runner

  # Set the number of parallel workers
  # Minitest::Test.number_of_workers = 4

  def test_parallel_safe
    # This test must be thread-safe if run in parallel
    assert_equal 42, 42
  end
end

# Custom test ordering
class TestOrderedExecution < Minitest::Test
  # Override to sort test methods alphabetically
  def self.runnable_methods
    super.sort
  end
end

Testing Patterns and Real-World Examples

Testing a Service Object

require 'minitest/autorun'

class UserRegistrationService
  def initialize(user_repository:, email_sender:, logger:)
    @user_repository = user_repository
    @email_sender = email_sender
    @logger = logger
  end

  def register(email:, password:, name:)
    raise ArgumentError, "Email required" if email.nil? || email.empty?
    raise ArgumentError, "Password too short" if password.length < 8

    user = @user_repository.create(email: email, password: password, name: name)

    @logger.info("User created: #{user.id}")
    @email_sender.send_welcome(email, name)

    { success: true, user: user }
  rescue => e
    @logger.error("Registration failed: #{e.message}")
    { success: false, error: e.message }
  end
end

class TestUserRegistrationService < Minitest::Test
  def setup
    @user_repo = Minitest::Mock.new
    @email_sender = Minitest::Mock.new
    @logger = Minitest::Mock.new
    @service = UserRegistrationService.new(
      user_repository: @user_repo,
      email_sender: @email_sender,
      logger: @logger
    )
  end

  def test_successful_registration
    user = OpenStruct.new(id: 123, email: "alice@example.com")

    @user_repo.expect :create, user, [{ email: "alice@example.com", password: "secure12345", name: "Alice" }]
    @logger.expect :info, true, ["User created: 123"]
    @email_sender.expect :send_welcome, true, ["alice@example.com", "Alice"]

    result = @service.register(
      email: "alice@example.com",
      password: "secure12345",
      name: "Alice"
    )

    assert_equal({ success: true, user: user }, result)
    @user_repo.verify
    @email_sender.verify
    @logger.verify
  end

  def test_registration_with_short_password
    @logger.expect :error, true, [/Password too short/]

    result = @service.register(
      email: "alice@example.com",
      password: "short",
      name: "Alice"
    )

    assert_equal false, result[:success]
    assert_match /Password too short/, result[:error]
    @logger.verify
  end

  def test_registration_with_missing_email
    @logger.expect :error, true, [/Email required/]

    result = @service.register(
      email: "",
      password: "secure12345",
      name: "Alice"
    )

    assert_equal false, result[:success]
    assert_match /Email required/, result[:error]
    @logger.verify
  end

  def test_handles_repository_exception_gracefully
    @user_repo.expect :create, nil do |args|
      raise "Database connection failed"
    end
    @logger.expect :error, true, [/Database connection failed/]

    result = @service.register(
      email: "alice@example.com",
      password: "secure12345",
      name: "Alice"
    )

    assert_equal false, result[:success]
    assert_match /Database connection failed/, result[:error]
    @user_repo.verify
    @logger.verify
  end
end

Testing a Value Object

class Money
  attr_reader :amount, :currency

  def initialize(amount, currency = "USD")
    @amount = amount.round(2)
    @currency = currency.upcase
  end

  def ==(other)
    other.is_a?(Money) && amount == other.amount && currency == other.currency
  end

  def +(other)
    raise CurrencyMismatchError unless currency == other.currency
    Money.new(amount + other.amount, currency)
  end

  def to_s
    "#{currency} #{format('%.2f', amount)}"
  end
end

class CurrencyMismatchError < StandardError; end

class TestMoney < Minitest::Test
  def test_equality
    five_dollars = Money.new(5.0, "USD")
    another_five = Money.new(5.0, "USD")
    assert_equal five_dollars, another_five
  end

  def test_inequality_different_amount
    refute_equal Money.new(5.0), Money.new(10.0)
  end

  def test_inequality_different_currency
    refute_equal Money.new(5.0, "USD"), Money.new(5.0, "EUR")
  end

  def test_addition_same_currency
    sum = Money.new(5.0) + Money.new(3.50)
    assert_equal Money.new(8.50), sum
  end

  def test_addition_different_currency_raises
    assert_raises(CurrencyMismatchError) do
      Money.new(5.0, "USD") + Money.new(3.0, "EUR")
    end
  end

  def test_rounding_on_initialization
    money = Money.new(5.678)
    assert_equal 5.68, money.amount
  end

  def test_currency_uppercase
    money = Money.new(5.0, "eur")
    assert_equal "EUR", money.currency
  end

  def test_string_representation
    money = Money.new(5.0, "USD")
    assert_equal "USD 5.00", money.to_s
  end
end

Testing Time-Dependent Code

class Subscription
  def initialize(created_at: Time.now)
    @created_at = created_at
  end

  def trial_active?
    (Time.now - @created_at) < (7 * 24 * 60 * 60)  # 7 days in seconds
  end

  def days_remaining
    elapsed = Time.now - @created_at
    remaining = (7 * 24 * 60 * 60) - elapsed
    [(remaining / (24 * 60 * 60)).ceil, 0].max
  end
end

class TestSubscription < Minitest::Test
  def test_trial_active_within_seven_days
    created = Time.new(2024, 1, 1, 12, 0, 0)
    now = Time.new(2024, 1, 6, 12, 0, 0)  # 5 days later

    Time.stub :now, now do
      subscription = Subscription.new(created_at: created)
      assert subscription.trial_active?
    end
  end

  def test_trial_expired_after_seven_days
    created = Time.new(2024, 1, 1, 12, 0, 0)
    now = Time.new(2024, 1, 9, 12, 0, 0)  # 8 days later

    Time.stub :now, now do
      subscription = Subscription.new(created_at: created)
      refute subscription.trial_active?
    end
  end

  def test_days_remaining_calculation
    created = Time.new(2024, 1, 1, 12, 0, 0)
    now = Time.new(2024, 1, 4, 12, 0, 0)  # 3 days later

    Time.stub :now, now do
      subscription = Subscription.new(created_at: created)
      assert_equal 4, subscription.days_remaining  # 7 - 3 = 4
    end
  end

  def test_days_remaining_never_negative
    created = Time.new(2024, 1, 1, 12, 0, 0)
    now = Time.new(2024, 1, 15, 12, 0, 0)  # 14 days later

    Time.stub :now, now do
      subscription = Subscription.new(created_at: created)
      assert_equal 0, subscription.days_remaining
    end
  end
end

Testing with Temporary File System

require 'tempfile'
require 'fileutils'

class FileProcessor
  def self.count_lines(file_path)
    File.readlines(file_path).size
  end

  def self.append_timestamp(file_path)
    timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
    File.open(file_path, 'a') do |file|
      file.puts "Processed at: #{timestamp}"
    end
    timestamp
  end
end

class TestFileProcessor < Minitest::Test
  def setup
    @tempfile = Tempfile.new(['test', '.txt'])
    @path = @tempfile.path
  end

  def teardown
    @tempfile.close
    @tempfile.unlink  # Delete the temp file
  end

  def test_count_lines
    File.write(@path, "line1\nline2\nline3\n")
    assert_equal 3, FileProcessor.count_lines(@path)
  end

  def test_count_empty_file
    File.write(@path, "")
    assert_equal 0, FileProcessor.count_lines(@path)
  end

  def test_append_timestamp
    File.write(@path, "original content\n")
    timestamp = FileProcessor.append_timestamp(@path)

    content = File.read(@path)
    assert_includes content, "original content"
    assert_includes content, "Processed at:"
    assert_includes content, timestamp
  end
end

Shared Context and Helper Modules

For larger test suites, Minitest supports shared behaviors through Ruby modules. This keeps tests DRY without requiring a heavyweight framework:

# test/support/authentication_helper.rb
module AuthenticationHelper
  def login_as(user)
    # Simulate authentication
    @current_user = user
  end

  def assert_authenticated
    refute_nil @current_user, "Expected user to be authenticated"
  end

  def assert_unauthorized_access
    assert_equal 403, @response.status if @response.respond_to?(:status)
  end
end

# test/admin_controller_test.rb
require 'minitest/autorun'
require_relative 'support/authentication_helper'

class TestAdminController < Minitest::Test
  include AuthenticationHelper

  def setup
    @controller = AdminController.new
  end

  def test_admin_access_with_valid_user
    login_as(User.new(role: :admin))
    assert_authenticated
    response = @controller.dashboard
    assert_equal 200, response.status
  end

  def test_admin_access_without_authentication
    response = @controller.dashboard
    assert_unauthorized_access
  end
end

Custom Assertions and Matchers

Building domain-specific assertions makes tests more readable and expressive. Minitest lets you extend its assertion vocabulary easily:

# Custom assertion module
module Assertions
  def assert_valid_email(email)
    pattern = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    assert_match pattern, email, "Expected #{email.inspect} to be a valid email address"
  end

  def assert_json_response(response)
    assert_equal "application/json", response.content_type
    data = JSON.parse(response.body)
    refute_nil data
    data
  end

  def assert_same_elements(expected, actual, msg = nil)
    msg ||= "Expected #{actual.inspect} to have same elements as #{expected.inspect}"
    assert_equal expected.sort, actual.sort, msg
  end
end

class TestCustomAssertions < Minitest::Test
  include Assertions

  def test_email_validation
    assert_valid_email "user

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles