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:
- Unit-style testing — classic xUnit pattern with test classes and assertion methods, familiar to developers coming from Test::Unit or JUnit
- Spec-style testing — BDD-flavored syntax with describe/it blocks and expectation matchers, reminiscent of RSpec's expressive style
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