CrackedRuby logo

CrackedRuby

Unit Testing

Comprehensive guide to unit testing frameworks, assertion methods, test organization, mocking strategies, and testing patterns in Ruby.

Testing and Quality Test Patterns
8.2.1

Overview

Unit testing in Ruby centers around writing isolated tests for individual methods, classes, and modules to verify expected behavior. Ruby provides multiple testing frameworks including the built-in Test::Unit, the popular RSpec with its behavior-driven development approach, and Minitest which combines unit and spec-style testing.

The core testing workflow involves defining test cases that exercise specific functionality, making assertions about expected outcomes, and organizing tests into logical groupings. Ruby's dynamic nature enables powerful testing techniques including runtime method redefinition, flexible mocking systems, and expressive assertion libraries.

# Test::Unit basic structure
require 'test/unit'

class CalculatorTest < Test::Unit::TestCase
  def test_addition
    calc = Calculator.new
    result = calc.add(2, 3)
    assert_equal 5, result
  end
end
# RSpec structure
RSpec.describe Calculator do
  describe '#add' do
    it 'returns the sum of two numbers' do
      calc = Calculator.new
      expect(calc.add(2, 3)).to eq(5)
    end
  end
end

Test::Unit ships with Ruby and follows traditional xUnit patterns with setup, teardown, and assert methods. RSpec provides domain-specific language for behavior-driven development with describe blocks and expectation syntax. Minitest offers both unit test and spec styles while maintaining compatibility with Test::Unit.

Basic Usage

Test::Unit requires inheriting from Test::Unit::TestCase and defining methods starting with test_. The framework automatically discovers and executes these test methods, providing assertion methods for verifying expected behavior.

require 'test/unit'

class StringProcessorTest < Test::Unit::TestCase
  def setup
    @processor = StringProcessor.new
  end

  def test_uppercase_conversion
    result = @processor.upcase('hello world')
    assert_equal 'HELLO WORLD', result
    assert_instance_of String, result
  end

  def test_word_count
    result = @processor.word_count('one two three')
    assert_equal 3, result
    assert result > 0
  end

  def teardown
    @processor = nil
  end
end

RSpec organizes tests using describe and context blocks to group related functionality, with it blocks containing individual test cases. The expectation syntax reads naturally and provides detailed failure messages.

RSpec.describe StringProcessor do
  let(:processor) { StringProcessor.new }

  describe '#upcase' do
    it 'converts lowercase text to uppercase' do
      expect(processor.upcase('hello')).to eq('HELLO')
    end

    it 'handles empty strings' do
      expect(processor.upcase('')).to eq('')
    end

    context 'with mixed case input' do
      it 'converts entire string to uppercase' do
        expect(processor.upcase('HeLLo WoRLd')).to eq('HELLO WORLD')
      end
    end
  end
end

Minitest provides two interfaces: traditional unit tests similar to Test::Unit and spec-style tests resembling RSpec. Both run under the same test runner and share assertion methods.

# Minitest unit style
require 'minitest/autorun'

class ProcessorTest < Minitest::Test
  def test_processing
    processor = DataProcessor.new
    assert_equal 42, processor.calculate(21, 2)
  end
end

# Minitest spec style
require 'minitest/autorun'

describe DataProcessor do
  it 'calculates correctly' do
    processor = DataProcessor.new
    _(processor.calculate(21, 2)).must_equal 42
  end
end

Test organization follows Ruby's module system. Tests typically mirror the source code structure with corresponding test files in a test/ or spec/ directory. The setup and teardown methods handle test initialization and cleanup, running before and after each test method respectively.

Advanced Usage

Ruby's metaprogramming capabilities enable sophisticated testing techniques including dynamic test generation, custom assertion methods, and runtime behavior modification. These patterns help reduce test duplication and create more maintainable test suites.

Dynamic test generation creates multiple similar tests programmatically, reducing repetitive code while maintaining test isolation:

class ValidationTest < Test::Unit::TestCase
  INVALID_EMAILS = [
    'invalid',
    'missing@domain',
    '@missinguser.com',
    'spaces in@email.com',
    'special!chars@domain.com'
  ]

  INVALID_EMAILS.each_with_index do |email, index|
    define_method("test_invalid_email_#{index}") do
      validator = EmailValidator.new
      assert_false validator.valid?(email), "#{email} should be invalid"
    end
  end
end

Custom assertion methods encapsulate complex verification logic and provide descriptive failure messages:

module CustomAssertions
  def assert_valid_json(string, message = nil)
    begin
      JSON.parse(string)
    rescue JSON::ParserError => e
      assert(false, message || "Expected valid JSON, got parse error: #{e.message}")
    end
  end

  def assert_responds_to_all(object, methods)
    missing = methods.reject { |method| object.respond_to?(method) }
    assert missing.empty?, "Object missing methods: #{missing.join(', ')}"
  end
end

class APITest < Test::Unit::TestCase
  include CustomAssertions

  def test_response_format
    response = api_client.get('/users')
    assert_valid_json(response.body)
    assert_responds_to_all(response, [:status, :headers, :body])
  end
end

RSpec's shared examples and contexts reduce duplication across similar test scenarios:

RSpec.shared_examples 'a validated model' do |model_class|
  let(:model) { model_class.new }

  it 'requires name to be present' do
    model.name = nil
    expect(model.valid?).to be false
    expect(model.errors[:name]).to include("can't be blank")
  end

  it 'requires name to be unique' do
    existing = model_class.create!(name: 'Test Name')
    model.name = 'Test Name'
    expect(model.valid?).to be false
    expect(model.errors[:name]).to include('has already been taken')
  end
end

RSpec.describe User do
  include_examples 'a validated model', User
end

RSpec.describe Product do
  include_examples 'a validated model', Product
end

Test doubles and stubbing control external dependencies during testing. Ruby provides multiple approaches for creating mock objects and stubbing method calls:

class WeatherServiceTest < Test::Unit::TestCase
  def test_temperature_conversion
    api_client = mock('api_client')
    api_client.expects(:get).with('/weather/current').returns(
      { 'temperature' => '72F', 'humidity' => '45%' }
    )
    
    service = WeatherService.new(api_client)
    temp = service.current_temperature_celsius
    assert_equal 22.2, temp.round(1)
  end
end

Testing Strategies

Effective unit testing requires strategic approaches to test organization, data management, and dependency isolation. Ruby testing frameworks provide multiple patterns for structuring tests that remain maintainable as codebases grow.

Test fixtures provide consistent data across test runs while avoiding brittle dependencies on external state. Ruby supports multiple fixture strategies from simple instance variables to complex factory systems:

class UserTest < Test::Unit::TestCase
  def setup
    @valid_user_attrs = {
      name: 'John Doe',
      email: 'john@example.com',
      age: 30
    }
    
    @user = User.new(@valid_user_attrs)
  end

  def test_full_name_generation
    assert_equal 'John Doe', @user.full_name
  end

  def test_adult_status
    assert @user.adult?
    
    child_user = User.new(@valid_user_attrs.merge(age: 15))
    assert_false child_user.adult?
  end
end

RSpec's let and let! declarations create lazy-loaded test data that gets memoized within individual test runs but resets between tests:

RSpec.describe Order do
  let(:customer) { Customer.new(name: 'Jane Smith') }
  let!(:product) { Product.create!(name: 'Widget', price: 10.00) }
  let(:order) { Order.new(customer: customer) }

  describe '#total' do
    before do
      order.add_item(product, quantity: 3)
      order.add_item(Product.create!(name: 'Gadget', price: 15.00), quantity: 2)
    end

    it 'calculates total from all items' do
      expect(order.total).to eq(60.00)
    end
  end
end

Test parameterization handles multiple input scenarios without duplicating test logic. Both frameworks support parameterized testing patterns:

class InputValidationTest < Test::Unit::TestCase
  VALIDATION_CASES = [
    { input: 'valid@email.com', expected: true, description: 'standard email' },
    { input: 'user+tag@domain.co.uk', expected: true, description: 'plus addressing' },
    { input: 'invalid.email', expected: false, description: 'missing @ symbol' },
    { input: '@domain.com', expected: false, description: 'missing username' },
    { input: 'user@', expected: false, description: 'missing domain' }
  ]

  VALIDATION_CASES.each do |test_case|
    define_method("test_email_validation_#{test_case[:description].gsub(/\s+/, '_')}") do
      validator = EmailValidator.new
      result = validator.valid?(test_case[:input])
      assert_equal test_case[:expected], result, 
        "Expected #{test_case[:input]} to be #{test_case[:expected] ? 'valid' : 'invalid'}"
    end
  end
end

Dependency injection and mocking isolate units under test from external dependencies. Ruby's duck typing enables flexible test double creation:

class PaymentProcessorTest < Test::Unit::TestCase
  def test_successful_payment_processing
    # Create test doubles
    gateway = mock('payment_gateway')
    logger = mock('logger')
    
    # Set expectations
    gateway.expects(:charge).with(
      amount: 100.00,
      card_token: 'tok_123'
    ).returns(
      success: true,
      transaction_id: 'txn_456'
    )
    
    logger.expects(:info).with('Payment processed: txn_456')
    
    # Execute test
    processor = PaymentProcessor.new(gateway: gateway, logger: logger)
    result = processor.process_payment(amount: 100.00, card_token: 'tok_123')
    
    assert result.success?
    assert_equal 'txn_456', result.transaction_id
  end
end

Error Handling & Debugging

Unit tests must handle both expected exceptions from the code under test and unexpected failures in the test infrastructure itself. Ruby testing frameworks provide assertion methods for verifying exception behavior and debugging test failures.

Exception testing verifies that methods raise appropriate errors under specific conditions. Test::Unit provides assert_raise and assert_nothing_raised for exception verification:

class FileProcessorTest < Test::Unit::TestCase
  def test_file_not_found_handling
    processor = FileProcessor.new
    
    assert_raise(FileNotFoundError) do
      processor.read_file('/nonexistent/path.txt')
    end
  end

  def test_invalid_format_handling
    processor = FileProcessor.new
    
    exception = assert_raise(InvalidFormatError) do
      processor.parse_csv('invalid,csv,content\nwith,missing')
    end
    
    assert_match /column count mismatch/, exception.message
    assert_equal 2, exception.line_number
  end

  def test_successful_processing
    processor = FileProcessor.new
    
    assert_nothing_raised do
      result = processor.read_file('test/fixtures/valid_data.csv')
      assert_instance_of Array, result
    end
  end
end

RSpec provides expect { }.to raise_error with flexible matching capabilities for exception class, message patterns, and custom matchers:

RSpec.describe FileProcessor do
  describe '#parse_csv' do
    it 'raises InvalidFormatError for malformed CSV' do
      processor = FileProcessor.new
      
      expect {
        processor.parse_csv("incomplete,csv\nrow")
      }.to raise_error(InvalidFormatError, /column count mismatch/)
    end

    it 'raises specific error with line information' do
      processor = FileProcessor.new
      
      expect {
        processor.parse_csv("header1,header2\nvalue1")
      }.to raise_error(an_instance_of(InvalidFormatError)
                         .and(having_attributes(line_number: 2)))
    end
  end
end

Test failure debugging requires systematic approaches to isolate the source of failures. Adding debugging output helps track test execution flow:

class ComplexCalculationTest < Test::Unit::TestCase
  def test_statistical_analysis
    data = [1, 2, 3, 4, 5, 100]  # Outlier intentionally included
    analyzer = StatisticalAnalyzer.new(data)
    
    # Debug intermediate calculations
    puts "Mean: #{analyzer.mean}"
    puts "Median: #{analyzer.median}"
    puts "Standard deviation: #{analyzer.standard_deviation}"
    
    # Multiple related assertions
    assert_in_delta 19.17, analyzer.mean, 0.01
    assert_equal 3.5, analyzer.median
    assert_in_delta 37.99, analyzer.standard_deviation, 0.01
  end
end

Advanced Usage

Advanced unit testing leverages Ruby's metaprogramming features, sophisticated mocking capabilities, and framework-specific extensions to create maintainable and expressive test suites.

Method aliasing and monkey patching enable temporary behavior modification during testing without affecting production code:

class TimeDependentTest < Test::Unit::TestCase
  def setup
    @original_now = Time.method(:now)
    @fixed_time = Time.new(2024, 1, 15, 12, 0, 0)
  end

  def test_time_sensitive_calculation
    # Stub Time.now for consistent results
    Time.define_singleton_method(:now) { @fixed_time }
    
    scheduler = TaskScheduler.new
    next_run = scheduler.next_execution_time
    
    assert_equal Time.new(2024, 1, 15, 13, 0, 0), next_run
  ensure
    # Restore original behavior
    Time.define_singleton_method(:now, &@original_now)
  end
end

RSpec's sophisticated mocking system supports method stubbing, spy verification, and complex expectation chains:

RSpec.describe NotificationService do
  describe '#send_welcome_email' do
    let(:email_client) { instance_double('EmailClient') }
    let(:template_engine) { instance_double('TemplateEngine') }
    let(:service) { NotificationService.new(email_client, template_engine) }
    let(:user) { User.new(name: 'Alice', email: 'alice@example.com') }

    before do
      allow(template_engine).to receive(:render)
        .with('welcome_template', user: user)
        .and_return('<h1>Welcome Alice!</h1>')
      
      allow(email_client).to receive(:send_email)
        .and_return(delivery_id: 'msg_123')
    end

    it 'renders template and sends email' do
      result = service.send_welcome_email(user)
      
      expect(template_engine).to have_received(:render)
        .with('welcome_template', user: user)
      
      expect(email_client).to have_received(:send_email)
        .with(
          to: 'alice@example.com',
          subject: 'Welcome!',
          body: '<h1>Welcome Alice!</h1>'
        )
      
      expect(result[:delivery_id]).to eq('msg_123')
    end
  end
end

Custom matchers extend testing frameworks with domain-specific assertions that improve test readability and provide better failure messages:

RSpec::Matchers.define :be_valid_credit_card do
  match do |number|
    @number = number.to_s.gsub(/\s+/, '')
    @number =~ /^\d{13,19}$/ && luhn_valid?(@number)
  end

  failure_message do
    if @number !~ /^\d{13,19}$/
      "expected #{@number} to contain 13-19 digits, got #{@number.length}"
    else
      "expected #{@number} to pass Luhn algorithm validation"
    end
  end

  def luhn_valid?(number)
    digits = number.chars.map(&:to_i).reverse
    sum = digits.each_with_index.sum do |digit, index|
      if index.odd?
        doubled = digit * 2
        doubled > 9 ? doubled - 9 : doubled
      else
        digit
      end
    end
    sum % 10 == 0
  end
end

# Usage
RSpec.describe CreditCard do
  it 'validates card numbers' do
    expect('4532123456789012').to be_valid_credit_card
    expect('1234567890123456').not_to be_valid_credit_card
  end
end

Shared contexts and modules provide reusable test infrastructure across multiple test files:

module DatabaseHelpers
  def with_clean_database
    DatabaseCleaner.start
    yield
  ensure
    DatabaseCleaner.clean
  end

  def create_test_user(attrs = {})
    User.create!({
      name: 'Test User',
      email: 'test@example.com'
    }.merge(attrs))
  end
end

class IntegrationTest < Test::Unit::TestCase
  include DatabaseHelpers

  def test_user_creation_workflow
    with_clean_database do
      user = create_test_user(name: 'Alice')
      assert_equal 'Alice', user.name
      assert user.persisted?
    end
  end
end

Error Handling & Debugging

Robust error handling in tests ensures clear failure messages and helps identify the root causes of test failures. Ruby testing frameworks provide multiple approaches for handling both expected and unexpected errors during test execution.

Assertion messages provide context when tests fail, making debugging faster and more accurate:

class ArrayProcessorTest < Test::Unit::TestCase
  def test_array_sorting_with_custom_comparator
    processor = ArrayProcessor.new
    input = [3, 1, 4, 1, 5, 9, 2, 6]
    
    result = processor.sort_with_comparator(input) { |a, b| b <=> a }
    expected = [9, 6, 5, 4, 3, 2, 1, 1]
    
    assert_equal expected, result, 
      "Expected descending sort, got #{result}. Original input: #{input}"
    
    # Verify original array unchanged
    assert_equal [3, 1, 4, 1, 5, 9, 2, 6], input,
      "Original array should not be modified"
  end
end

Exception rescue blocks in tests handle setup failures and provide fallback behavior when external dependencies are unavailable:

class ExternalServiceTest < Test::Unit::TestCase
  def setup
    begin
      @service = ExternalAPIClient.new
      @service.ping  # Verify connectivity
      @service_available = true
    rescue ConnectionError, TimeoutError => e
      @service_available = false
      @skip_message = "External service unavailable: #{e.message}"
    end
  end

  def test_api_data_retrieval
    skip(@skip_message) unless @service_available
    
    data = @service.fetch_user_data(user_id: 123)
    assert_instance_of Hash, data
    assert data.key?('name')
    assert data.key?('email')
  end
end

RSpec's aggregate failures collect multiple assertion failures within a single test, providing comprehensive failure information:

RSpec.describe UserProfile do
  describe '#validate' do
    it 'validates all required fields' do
      profile = UserProfile.new
      result = profile.validate
      
      aggregate_failures 'validation results' do
        expect(result.valid?).to be false
        expect(result.errors).to include('Name is required')
        expect(result.errors).to include('Email is required')
        expect(result.errors).to include('Age must be positive')
        expect(result.error_count).to eq(3)
      end
    end
  end
end

Debugging complex test failures often requires inspecting object state and method call sequences. Ruby's introspection capabilities combined with testing framework hooks provide powerful debugging tools:

class StateTrackingTest < Test::Unit::TestCase
  def setup
    @call_log = []
    @original_methods = {}
  end

  def track_method_calls(object, method_name)
    @original_methods[object] ||= {}
    @original_methods[object][method_name] = object.method(method_name)
    
    object.define_singleton_method(method_name) do |*args, **kwargs|
      @call_log << { 
        object: object.class.name, 
        method: method_name, 
        args: args, 
        kwargs: kwargs,
        timestamp: Time.now 
      }
      @original_methods[object][method_name].call(*args, **kwargs)
    end
  end

  def test_complex_interaction_sequence
    processor = DataProcessor.new
    validator = DataValidator.new
    
    track_method_calls(processor, :normalize)
    track_method_calls(validator, :validate)
    
    pipeline = ProcessingPipeline.new(processor, validator)
    pipeline.process(['raw', 'data', 'items'])
    
    # Verify call sequence
    assert_equal 3, @call_log.count { |log| log[:method] == :normalize }
    assert_equal 1, @call_log.count { |log| log[:method] == :validate }
    
    # Print debug information on failure
    if @call_log.empty?
      flunk "No method calls were tracked. Pipeline: #{pipeline.inspect}"
    end
  end

  def teardown
    @original_methods.each do |object, methods|
      methods.each do |method_name, original_method|
        object.define_singleton_method(method_name, &original_method)
      end
    end
  end
end

Common Pitfalls

Unit testing in Ruby presents several common pitfalls that can lead to brittle, slow, or unreliable tests. Understanding these issues helps create more maintainable test suites.

Test interdependence occurs when tests rely on execution order or shared state, causing failures when tests run in different sequences:

# PROBLEMATIC: Tests depend on execution order
class ProblematicTest < Test::Unit::TestCase
  @@user_count = 0

  def test_user_creation
    @@user_count += 1
    user = User.create!(name: "User #{@@user_count}")
    assert_equal "User 1", user.name  # Fails if run after other tests
  end

  def test_user_deletion
    User.create!(name: "User #{@@user_count + 1}")
    @@user_count += 1
    
    User.delete_all
    assert_equal 0, User.count  # Depends on previous test state
  end
end

# BETTER: Isolated tests with proper setup
class ImprovedTest < Test::Unit::TestCase
  def setup
    User.delete_all  # Clean slate for each test
  end

  def test_user_creation
    user = User.create!(name: "Test User")
    assert_equal "Test User", user.name
    assert_equal 1, User.count
  end

  def test_user_deletion
    user = User.create!(name: "Test User")
    user.delete
    assert_equal 0, User.count
  end
end

Over-mocking reduces test value by mocking the system under test instead of its dependencies. Tests should verify real behavior rather than mock interactions:

# PROBLEMATIC: Over-mocked test
class OverMockedTest < Test::Unit::TestCase
  def test_calculation_engine
    engine = mock('calculation_engine')
    engine.expects(:add).with(2, 3).returns(5)
    engine.expects(:multiply).with(5, 2).returns(10)
    
    # This test verifies mock setup, not actual calculation logic
    result = engine.add(2, 3)
    final = engine.multiply(result, 2)
    assert_equal 10, final
  end
end

# BETTER: Test real object, mock dependencies only
class ProperTest < Test::Unit::TestCase
  def test_calculation_engine
    logger = mock('logger')
    logger.expects(:debug).with('Performing calculation')
    
    engine = CalculationEngine.new(logger: logger)
    result = engine.calculate(2, 3, :add_then_double)
    
    assert_equal 10, result  # Verifies actual calculation logic
  end
end

Testing implementation details instead of behavior creates brittle tests that break during refactoring. Focus on observable outcomes rather than internal method calls:

# PROBLEMATIC: Testing implementation details
RSpec.describe ShoppingCart do
  it 'calls internal calculation methods' do
    cart = ShoppingCart.new
    allow(cart).to receive(:calculate_subtotal)
    allow(cart).to receive(:apply_discounts)
    allow(cart).to receive(:calculate_tax)
    
    cart.total_price
    
    expect(cart).to have_received(:calculate_subtotal)
    expect(cart).to have_received(:apply_discounts)
    expect(cart).to have_received(:calculate_tax)
  end
end

# BETTER: Testing behavior and outcomes
RSpec.describe ShoppingCart do
  describe '#total_price' do
    let(:cart) { ShoppingCart.new }
    
    before do
      cart.add_item(Product.new(price: 10.00), quantity: 2)
      cart.add_item(Product.new(price: 5.00), quantity: 1)
    end

    it 'calculates total with tax and discounts' do
      cart.apply_discount_code('SAVE10')  # 10% off
      total = cart.total_price
      
      # Subtotal: $25, 10% discount: $22.50, 8% tax: $24.30
      expect(total).to be_within(0.01).of(24.30)
    end
  end
end

Resource leakage between tests occurs when tests fail to clean up files, network connections, or modified global state:

class ResourceManagementTest < Test::Unit::TestCase
  def setup
    @temp_files = []
    @original_env = ENV.to_h.dup
  end

  def test_file_processing_with_cleanup
    # Create temporary file
    temp_file = Tempfile.new('test_data')
    @temp_files << temp_file
    temp_file.write("test,data\n1,2\n3,4")
    temp_file.close
    
    processor = FileProcessor.new
    result = processor.process_csv(temp_file.path)
    
    assert_equal 2, result.length
    assert_equal ['1', '2'], result.first
  end

  def test_environment_variable_modification
    ENV['TEST_MODE'] = 'true'
    ENV['DEBUG_LEVEL'] = 'verbose'
    
    config = AppConfiguration.new
    assert config.test_mode?
    assert_equal 'verbose', config.debug_level
  end

  def teardown
    # Clean up temporary files
    @temp_files.each do |file|
      file.unlink if file.respond_to?(:unlink) && File.exist?(file.path)
    end
    
    # Restore environment variables
    ENV.clear
    ENV.update(@original_env)
  end
end

Reference

Test::Unit Assertion Methods

Method Parameters Returns Description
assert(condition, message = nil) condition (Object), message (String) void Verifies condition is truthy
assert_equal(expected, actual, message = nil) expected (Object), actual (Object), message (String) void Verifies equality using ==
assert_not_equal(expected, actual, message = nil) expected (Object), actual (Object), message (String) void Verifies inequality
assert_nil(object, message = nil) object (Object), message (String) void Verifies object is nil
assert_not_nil(object, message = nil) object (Object), message (String) void Verifies object is not nil
assert_instance_of(class, object, message = nil) class (Class), object (Object), message (String) void Verifies exact class match
assert_kind_of(class, object, message = nil) class (Class), object (Object), message (String) void Verifies class or subclass match
assert_respond_to(object, method, message = nil) object (Object), method (Symbol), message (String) void Verifies method availability
assert_match(pattern, string, message = nil) pattern (Regexp), string (String), message (String) void Verifies regex pattern match
assert_no_match(pattern, string, message = nil) pattern (Regexp), string (String), message (String) void Verifies no regex match
assert_in_delta(expected, actual, delta, message = nil) expected (Numeric), actual (Numeric), delta (Numeric), message (String) void Verifies numeric values within delta
assert_raise(exception_class, message = nil) { block } exception_class (Class), message (String), block (Proc) Exception Verifies block raises specific exception
assert_nothing_raised(message = nil) { block } message (String), block (Proc) void Verifies block executes without exceptions
assert_throws(symbol, message = nil) { block } symbol (Symbol), message (String), block (Proc) void Verifies block throws specific symbol

RSpec Expectation Methods

Method Parameters Returns Description
expect(actual).to matcher actual (Object), matcher (Matcher) void Positive expectation
expect(actual).not_to matcher actual (Object), matcher (Matcher) void Negative expectation
expect { block }.to matcher block (Proc), matcher (Matcher) void Block expectation
expect(actual).to eq(expected) actual (Object), expected (Object) void Equality using ==
expect(actual).to eql(expected) actual (Object), expected (Object) void Equality using eql?
expect(actual).to equal(expected) actual (Object), expected (Object) void Identity using equal?
expect(actual).to be_instance_of(class) actual (Object), class (Class) void Exact class verification
expect(actual).to be_a(class) actual (Object), class (Class) void Class or subclass verification
expect(actual).to respond_to(method) actual (Object), method (Symbol) void Method availability
expect(actual).to match(pattern) actual (String), pattern (Regexp) void Regex pattern matching
expect(actual).to be_within(delta).of(expected) actual (Numeric), delta (Numeric), expected (Numeric) void Numeric approximation
expect { block }.to raise_error(class, message) block (Proc), class (Class), message (String/Regexp) void Exception verification
expect { block }.to change(object, method) block (Proc), object (Object), method (Symbol) void State change verification

Minitest Assertion Methods

Method Parameters Returns Description
assert(condition, message = nil) condition (Object), message (String) void Basic truthiness assertion
assert_equal(expected, actual, message = nil) expected (Object), actual (Object), message (String) void Equality verification
assert_in_delta(expected, actual, delta = 0.001, message = nil) expected (Float), actual (Float), delta (Float), message (String) void Float comparison with tolerance
assert_includes(collection, object, message = nil) collection (Enumerable), object (Object), message (String) void Collection membership
assert_match(pattern, string, message = nil) pattern (Regexp), string (String), message (String) void Pattern matching
assert_operator(object1, operator, object2, message = nil) object1 (Object), operator (Symbol), object2 (Object), message (String) void Operator-based comparison
assert_predicate(object, predicate, message = nil) object (Object), predicate (Symbol), message (String) void Predicate method verification
assert_raises(exception_class, message = nil) { block } exception_class (Class), message (String), block (Proc) Exception Exception verification
refute(condition, message = nil) condition (Object), message (String) void Negative assertion
refute_equal(expected, actual, message = nil) expected (Object), actual (Object), message (String) void Inequality verification

Framework Configuration Options

Framework Configuration Method Purpose
Test::Unit Test::Unit.at_start { block } Code execution before test suite
Test::Unit Test::Unit.at_exit { block } Code execution after test suite
RSpec `RSpec.configure { config
RSpec config.before(:each) { block } Before each test execution
RSpec config.after(:each) { block } After each test execution
RSpec `config.around(:each) { example
Minitest Minitest.after_run { block } Post-suite execution hook

Common Test Patterns

Pattern Framework Implementation
Setup/Teardown Test::Unit def setup / def teardown methods
Lazy Loading RSpec let(:name) { expression }
Eager Loading RSpec let!(:name) { expression }
Shared Examples RSpec shared_examples 'name' / include_examples
Parameterized Tests All Dynamic method definition with data arrays
Mock Expectations RSpec expect(object).to receive(:method)
Stub Behavior RSpec allow(object).to receive(:method).and_return(value)
Exception Testing Test::Unit assert_raise(ExceptionClass) { block }
State Change Testing RSpec expect { block }.to change(object, :method)

Test Execution Commands

Framework Command Purpose
Test::Unit ruby test/test_file.rb Run single test file
Test::Unit ruby -Itest test/test_*.rb Run all tests in directory
RSpec rspec spec/file_spec.rb Run single spec file
RSpec rspec spec/ Run all specs in directory
RSpec rspec --format documentation Detailed output format
Minitest ruby -Itest test/test_file.rb Run single test file
Minitest rake test Run all tests via Rake
All --verbose Detailed test execution output