CrackedRuby logo

CrackedRuby

MiniTest

Complete guide to MiniTest testing framework in Ruby, covering test definition, assertions, organization, debugging, and production usage patterns.

Testing and Quality Testing Frameworks
8.1.2

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_setupsetupafter_setup
Test Test method execution
Teardown before_teardownteardownafter_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