Overview
MiniTest provides a complete testing framework for Ruby applications through two distinct interfaces: a traditional unit testing framework similar to TestUnit and a spec-style DSL. The framework includes built-in support for unit tests, mock objects, and benchmarking within Ruby's standard library.
The core of MiniTest revolves around the Minitest::Test
class for unit-style tests and Minitest::Spec
for spec-style tests. Both approaches compile to the same underlying test runner, giving developers flexibility in testing style while maintaining consistent execution behavior.
# Unit-style testing
class CalculatorTest < Minitest::Test
def test_addition
result = Calculator.new.add(2, 3)
assert_equal 5, result
end
end
# Spec-style testing
describe Calculator do
it "adds two numbers correctly" do
result = Calculator.new.add(2, 3)
_(result).must_equal 5
end
end
MiniTest executes tests through the Minitest.run
method, which handles test discovery, execution order, and result reporting. The framework automatically discovers test files following naming conventions and provides detailed failure reporting with stack traces and assertion context.
Basic Usage
Test files require the minitest/autorun
library, which automatically executes tests when the file runs. Test classes inherit from Minitest::Test
and define test methods beginning with test_
.
require 'minitest/autorun'
class StringProcessorTest < Minitest::Test
def setup
@processor = StringProcessor.new
end
def test_uppercase_conversion
result = @processor.upcase("hello world")
assert_equal "HELLO WORLD", result
end
def test_word_count
result = @processor.word_count("the quick brown fox")
assert_equal 4, result
end
def teardown
@processor = nil
end
end
The setup
method runs before each test, while teardown
runs after each test completes. These methods ensure test isolation by preparing fresh state and cleaning up resources.
MiniTest provides assertion methods for different comparison types. The assert
method accepts any truthy expression, while specialized assertions offer more descriptive failure messages.
def test_various_assertions
# Basic truth assertions
assert true
refute false
assert_nil nil
refute_nil "not nil"
# Equality and comparison
assert_equal "expected", "actual"
refute_equal "not this", "actual"
assert_in_delta 3.14, Math::PI, 0.01
# Pattern matching
assert_match /\d+/, "contains 123 numbers"
refute_match /[A-Z]/, "lowercase only"
# Collection assertions
assert_includes [1, 2, 3], 2
assert_empty []
refute_empty [1]
end
The spec-style DSL provides equivalent functionality through expectation methods. The underscore method _()
wraps values for chaining expectations.
require 'minitest/autorun'
require 'minitest/spec'
describe "String operations" do
before do
@text = "Hello World"
end
it "converts to uppercase" do
result = @text.upcase
_(result).must_equal "HELLO WORLD"
_(result).must_match /HELLO/
end
it "counts characters correctly" do
_(@text.length).must_equal 11
_(@text).wont_be_empty
end
end
Running tests produces output showing test count, execution time, and any failures. The default reporter shows dots for passing tests and letters for failures or skips.
Advanced Usage
MiniTest supports custom assertion methods through module inclusion or direct definition within test classes. Custom assertions should follow the pattern of calling existing assertions and providing descriptive failure messages.
module CustomAssertions
def assert_valid_email(email)
pattern = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
assert_match pattern, email, "Expected valid email format, got: #{email}"
end
def assert_responds_with_json(response)
assert_equal 'application/json', response.content_type
assert_nothing_raised(JSON::ParserError) { JSON.parse(response.body) }
end
end
class EmailValidatorTest < Minitest::Test
include CustomAssertions
def test_email_validation
assert_valid_email "user@example.com"
assert_responds_with_json mock_api_response
end
end
Test organization benefits from shared setup through class-level methods and module inclusion. The parallelize_me!
directive enables parallel test execution for improved performance on multi-core systems.
class DatabaseTest < Minitest::Test
parallelize_me!
def self.test_order
:alpha # or :random, :parallel
end
def self.startup
DatabaseCleaner.clean_with(:truncation)
end
def self.shutdown
DatabaseCleaner.clean_with(:deletion)
end
def setup
DatabaseCleaner.start
end
def teardown
DatabaseCleaner.clean
end
end
MiniTest hooks provide control over test lifecycle through before_setup
, after_setup
, before_teardown
, and after_teardown
methods. These hooks execute in specific order and allow for complex test environment preparation.
class IntegrationTest < Minitest::Test
def before_setup
super
start_test_server
end
def after_setup
configure_test_client
super
end
def before_teardown
super
capture_test_logs
end
def after_teardown
stop_test_server
super
end
end
The framework supports test skipping and conditional execution through skip
calls and conditional test definition. Tests can be skipped programmatically based on environment conditions or feature availability.
def test_redis_integration
skip "Redis not available" unless redis_available?
redis = Redis.new
redis.set("test_key", "test_value")
assert_equal "test_value", redis.get("test_key")
end
def test_platform_specific_feature
skip "Windows only feature" unless Gem.win_platform?
assert_equal expected_result, windows_specific_method
end
Testing Strategies
MiniTest encourages test organization through descriptive test names and logical grouping. Test methods should focus on single behaviors while maintaining readable names that describe the expected outcome.
class UserRegistrationTest < Minitest::Test
def test_successful_registration_creates_user_record
user_count_before = User.count
result = UserRegistration.new(valid_user_params).call
assert result.success?
assert_equal user_count_before + 1, User.count
assert_equal "john@example.com", result.user.email
end
def test_duplicate_email_registration_fails_with_error
User.create!(email: "existing@example.com")
result = UserRegistration.new(email: "existing@example.com").call
refute result.success?
assert_includes result.errors, "Email already taken"
end
def test_invalid_email_format_prevents_registration
invalid_emails = ["invalid", "@example.com", "user@", "user@.com"]
invalid_emails.each do |email|
result = UserRegistration.new(email: email).call
refute result.success?, "Expected #{email} to be invalid"
end
end
end
Mock objects through Minitest::Mock
enable isolated testing by replacing dependencies with controlled test doubles. Mocks track method calls and return predetermined values, allowing verification of object interactions.
def test_payment_processing_calls_gateway
gateway_mock = Minitest::Mock.new
gateway_mock.expect :charge, true, [1000, "card_token"]
gateway_mock.expect :send_receipt, nil, [String]
processor = PaymentProcessor.new(gateway: gateway_mock)
result = processor.process_payment(amount: 1000, token: "card_token")
assert result
gateway_mock.verify # Ensures all expected calls occurred
end
def test_api_client_handles_timeout
http_mock = Minitest::Mock.new
http_mock.expect :get, nil do |url|
raise Net::TimeoutError, "Request timeout"
end
client = ApiClient.new(http: http_mock)
assert_raises(ApiClient::TimeoutError) do
client.fetch_data("/api/endpoint")
end
http_mock.verify
end
Stub methods provide simpler mocking for individual method calls without full mock object creation. Stubs temporarily replace method implementations and automatically restore original behavior after test completion.
def test_time_dependent_logic
fixed_time = Time.new(2024, 6, 15, 12, 0, 0)
Time.stub :now, fixed_time do
result = TimeBasedCalculator.current_period
assert_equal "Q2", result
end
# Time.now returns normal behavior after block
end
def test_external_service_unavailable
ExternalService.stub :available?, false do
result = ServiceChecker.new.status
assert_equal :unavailable, result.state
assert_equal "Service temporarily unavailable", result.message
end
end
Test data setup benefits from factory methods and shared fixtures. Creating reusable data builders reduces duplication and improves test maintainability.
class OrderTest < Minitest::Test
private
def create_order(attributes = {})
defaults = {
customer_id: 1,
total: 100.00,
status: :pending,
items: [create_item]
}
Order.new(defaults.merge(attributes))
end
def create_item(attributes = {})
defaults = { name: "Test Item", price: 25.00, quantity: 1 }
OrderItem.new(defaults.merge(attributes))
end
def test_order_total_calculation
order = create_order(items: [
create_item(price: 30.00, quantity: 2),
create_item(price: 15.00, quantity: 1)
])
assert_equal 75.00, order.calculate_total
end
end
Production Patterns
Rails applications integrate MiniTest through the default test suite structure. The rails test
command automatically discovers and executes tests in the test directory, providing convenient filtering and reporting options.
# test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
test "validates email format" do
user = User.new(email: "invalid-email")
assert_not user.valid?
assert_includes user.errors[:email], "is invalid"
end
test "creates user with valid attributes" do
user = User.create!(
email: "test@example.com",
password: "secure_password"
)
assert_predicate user, :persisted?
assert_equal "test@example.com", user.email
end
end
# test/controllers/users_controller_test.rb
class UsersControllerTest < ActionDispatch::IntegrationTest
test "GET /users returns user list" do
create_users(3)
get users_path
assert_response :success
assert_select 'tr.user-row', count: 3
end
test "POST /users creates new user" do
assert_difference 'User.count', 1 do
post users_path, params: { user: valid_user_params }
end
assert_redirected_to user_path(User.last)
end
end
Continuous integration environments benefit from MiniTest's built-in reporters and output formatting. The framework supports JUnit XML output for CI systems and provides hooks for custom reporting.
# test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'
require 'minitest/reporters'
if ENV['CI']
Minitest::Reporters.use! Minitest::Reporters::JUnitReporter.new
else
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
end
class ActiveSupport::TestCase
fixtures :all
def setup
DatabaseCleaner.start
end
def teardown
DatabaseCleaner.clean
end
private
def sign_in_as(user)
post login_path, params: { email: user.email, password: 'password' }
end
end
Large test suites benefit from parallel execution and selective test running. MiniTest supports test filtering through name patterns, line numbers, and custom tags.
# Run specific test method
# ruby test/user_test.rb -n test_email_validation
# Run tests matching pattern
# ruby test/user_test.rb -n /email/
# Run test at specific line
# ruby test/user_test.rb:25
class TaggedTest < Minitest::Test
def test_fast_operation
# Fast test that runs in CI
end
def test_slow_integration
skip "Slow test" if ENV['FAST_TESTS_ONLY']
# Slow integration test
end
end
Performance monitoring in production test suites uses MiniTest's built-in benchmarking and custom reporters. Test execution metrics help identify slow tests and optimize suite performance.
class PerformanceTest < Minitest::Test
def test_query_performance
benchmark = Minitest::Benchmark.new
time = benchmark.measure do
User.joins(:orders).where(created_at: 1.year.ago..Time.now).count
end
assert_operator time, :<, 0.1, "Query took too long: #{time}s"
end
end
# Custom reporter for tracking test metrics
class MetricsReporter < Minitest::AbstractReporter
def record(result)
if result.time > 5.0
puts "Slow test detected: #{result.name} (#{result.time}s)"
end
end
end
Common Pitfalls
Test isolation failures occur when tests depend on execution order or shared state. MiniTest randomizes test execution by default, exposing these dependencies that may not appear during sequential runs.
# PROBLEMATIC: Tests depend on execution order
class BadTest < Minitest::Test
@@counter = 0 # Shared class variable
def test_first_increment
@@counter += 1
assert_equal 1, @@counter # Fails if test_second runs first
end
def test_second_increment
@@counter += 1
assert_equal 2, @@counter # Depends on first test running
end
end
# CORRECTED: Each test sets up independent state
class GoodTest < Minitest::Test
def test_counter_increments_from_zero
counter = Counter.new
counter.increment
assert_equal 1, counter.value
end
def test_counter_increments_multiple_times
counter = Counter.new
2.times { counter.increment }
assert_equal 2, counter.value
end
end
Assertion selection affects failure message clarity and debugging effectiveness. Generic assertions like assert
provide minimal context compared to specific assertions that describe expected relationships.
# UNCLEAR: Generic assertion with poor failure message
def test_user_creation_bad
user = User.create(email: "test@example.com")
assert user.id.is_a?(Integer) # "Expected true, got false"
end
# CLEAR: Specific assertion with descriptive failure
def test_user_creation_good
user = User.create(email: "test@example.com")
assert_kind_of Integer, user.id
refute_nil user.id
assert_operator user.id, :>, 0
end
Mock verification must occur after all expected method calls complete. Calling mock.verify
too early or forgetting verification entirely leads to false test passes when mocked methods never execute.
# INCORRECT: Early verification misses actual method calls
def test_broken_mock_verification
service_mock = Minitest::Mock.new
service_mock.expect :call, true, ["data"]
service_mock.verify # Verifies too early!
result = ServiceCaller.new(service_mock).execute("data")
# Mock verification already passed, missing actual verification
end
# CORRECT: Verification after all interactions
def test_proper_mock_verification
service_mock = Minitest::Mock.new
service_mock.expect :call, true, ["data"]
service_mock.expect :cleanup, nil, []
caller = ServiceCaller.new(service_mock)
result = caller.execute("data")
caller.cleanup
service_mock.verify # Verifies all expected calls occurred
end
Spec-style testing requires careful attention to context and variable scoping. Instance variables defined in before
blocks remain available in test methods, but local variables do not persist across block boundaries.
describe "Variable scoping issues" do
before do
@instance_var = "available in tests"
local_var = "not available in tests" # Lost after before block
end
it "accesses instance variables correctly" do
_(@instance_var).must_equal "available in tests" # Works
# _(local_var).must_equal "anything" # NameError: undefined local variable
end
it "handles local variables through methods" do
data = create_test_data # Method call works
_(data).wont_be_nil
end
private
def create_test_data
"method-created data"
end
end
Exception testing requires precise exception class matching and message verification. Using overly broad exception catching or incorrect exception types leads to tests passing when they should fail.
# IMPRECISE: Catches any StandardError
def test_imprecise_exception
assert_raises(StandardError) do # Too broad
DivisionCalculator.divide(10, 0)
end
end
# PRECISE: Tests specific exception and message
def test_precise_exception
exception = assert_raises(ZeroDivisionError) do
DivisionCalculator.divide(10, 0)
end
assert_equal "Cannot divide by zero", exception.message
assert_includes exception.backtrace.first, "division_calculator.rb"
end
Reference
Core Classes and Modules
Class/Module | Purpose | Key Methods |
---|---|---|
Minitest::Test |
Unit-style test base class | setup , teardown , assert_* , refute_* |
Minitest::Spec |
Spec-style test base class | before , after , describe , it |
Minitest::Mock |
Mock object implementation | expect , verify , new |
Minitest::Stub |
Method stubbing utilities | stub |
Minitest::Benchmark |
Performance testing | bench_* , assert_performance_* |
Assertion Methods
Method | Parameters | Returns | Description |
---|---|---|---|
assert(test, msg = nil) |
test (Object), msg (String) | TrueClass |
Fails unless test is truthy |
assert_equal(exp, act, msg = nil) |
exp (Object), act (Object), msg (String) | TrueClass |
Fails unless exp == act |
assert_in_delta(exp, act, delta, msg = nil) |
exp (Numeric), act (Numeric), delta (Numeric), msg (String) | TrueClass |
Fails unless (exp - act).abs <= delta |
assert_includes(collection, obj, msg = nil) |
collection (Enumerable), obj (Object), msg (String) | TrueClass |
Fails unless collection includes obj |
assert_instance_of(klass, obj, msg = nil) |
klass (Class), obj (Object), msg (String) | TrueClass |
Fails unless obj.class == klass |
assert_kind_of(klass, obj, msg = nil) |
klass (Class), obj (Object), msg (String) | TrueClass |
Fails unless obj.kind_of?(klass) |
assert_match(matcher, obj, msg = nil) |
matcher (Regexp/String), obj (String), msg (String) | TrueClass |
Fails unless matcher =~ obj |
assert_nil(obj, msg = nil) |
obj (Object), msg (String) | TrueClass |
Fails unless obj.nil? |
assert_operator(o1, op, o2, msg = nil) |
o1 (Object), op (Symbol), o2 (Object), msg (String) | TrueClass |
Fails unless o1.send(op, o2) |
assert_raises(*exp, &block) |
exp (Class), block (Proc) | Exception |
Fails unless block raises expected exception |
assert_respond_to(obj, meth, msg = nil) |
obj (Object), meth (Symbol/String), msg (String) | TrueClass |
Fails unless obj.respond_to?(meth) |
Refutation Methods
Method | Parameters | Returns | Description |
---|---|---|---|
refute(test, msg = nil) |
test (Object), msg (String) | TrueClass |
Fails if test is truthy |
refute_equal(exp, act, msg = nil) |
exp (Object), act (Object), msg (String) | TrueClass |
Fails if exp == act |
refute_includes(collection, obj, msg = nil) |
collection (Enumerable), obj (Object), msg (String) | TrueClass |
Fails if collection includes obj |
refute_instance_of(klass, obj, msg = nil) |
klass (Class), obj (Object), msg (String) | TrueClass |
Fails if obj.class == klass |
refute_match(matcher, obj, msg = nil) |
matcher (Regexp/String), obj (String), msg (String) | TrueClass |
Fails if matcher =~ obj |
refute_nil(obj, msg = nil) |
obj (Object), msg (String) | TrueClass |
Fails if obj.nil? |
Spec-Style Expectations
Expectation | Equivalent Assertion | Description |
---|---|---|
_(obj).must_equal(exp) |
assert_equal(exp, obj) |
Object equality |
_(obj).wont_equal(exp) |
refute_equal(exp, obj) |
Object inequality |
_(obj).must_be_nil |
assert_nil(obj) |
Nil check |
_(obj).wont_be_nil |
refute_nil(obj) |
Non-nil check |
_(obj).must_match(regex) |
assert_match(regex, obj) |
Pattern matching |
_(obj).must_include(item) |
assert_includes(obj, item) |
Collection membership |
_(obj).must_be_kind_of(klass) |
assert_kind_of(klass, obj) |
Type checking |
_(obj).must_respond_to(method) |
assert_respond_to(obj, method) |
Method availability |
Command Line Options
Option | Description | Example |
---|---|---|
-n PATTERN |
Run tests matching name pattern | ruby test.rb -n /user/ |
-s SEED |
Set random seed for test order | ruby test.rb -s 12345 |
-v |
Verbose output with test names | ruby test.rb -v |
--pride |
Colorful progress output | ruby test.rb --pride |
-e PATTERN |
Exclude tests matching pattern | ruby test.rb -e /slow/ |
Environment Variables
Variable | Effect | Values |
---|---|---|
MT_CPU |
Control parallel test execution | Integer (number of cores) |
TESTOPTS |
Pass options to test runner | String of command line options |
MINITEST_REPORTER |
Set default reporter | SpecReporter , ProgressReporter |
Hook Execution Order
Phase | Methods (in execution order) |
---|---|
Setup | before_setup → setup → after_setup |
Test | Test method execution |
Teardown | before_teardown → teardown → after_teardown |
Mock Method Signatures
Method | Parameters | Description |
---|---|---|
expect(name, retval, args = [], &block) |
name (Symbol), retval (Object), args (Array), block (Proc) | Define expected method call |
verify |
None | Verify all expected calls occurred |
defined?(name) |
name (Symbol) | Check if method expectation exists |