CrackedRuby logo

CrackedRuby

Test::Unit

Comprehensive guide to Test::Unit testing framework in Ruby, covering basic usage, advanced patterns, error handling, testing strategies, and production deployment.

Testing and Quality Testing Frameworks
8.1.3

Overview

Test::Unit provides Ruby's built-in unit testing framework for writing and running automated tests. The framework centers on the Test::Unit::TestCase class, which developers subclass to create test suites containing individual test methods.

Test::Unit operates on a simple principle: each test method starts with test_ and contains assertions that verify expected behavior. The framework runs tests automatically, reporting successes and failures with detailed output. Ruby includes Test::Unit in its standard library, making it immediately available without external dependencies.

require 'test/unit'

class CalculatorTest < Test::Unit::TestCase
  def test_addition
    assert_equal 4, 2 + 2
  end
  
  def test_division
    assert_equal 2.5, 5.0 / 2
  end
end

The framework discovers test methods automatically when classes inherit from Test::Unit::TestCase. Test methods execute in isolation, with setup and teardown hooks available for test preparation and cleanup. Test::Unit reports results through multiple output formats and provides detailed failure messages with stack traces.

class StringTest < Test::Unit::TestCase
  def setup
    @text = "Hello World"
  end
  
  def test_length
    assert_equal 11, @text.length
  end
  
  def test_case_conversion
    assert_equal "HELLO WORLD", @text.upcase
    assert_equal "hello world", @text.downcase
  end
end

Test::Unit integrates with Ruby's exception handling system, catching assertion failures and unexpected errors during test execution. The framework supports test organization through nested test classes and provides hooks for complex test scenarios requiring shared state or external resource management.

Basic Usage

Test creation begins by requiring the Test::Unit library and subclassing Test::Unit::TestCase. Each test method must start with test_ for automatic discovery during test runs.

require 'test/unit'

class MathOperationsTest < Test::Unit::TestCase
  def test_multiplication
    result = 6 * 7
    assert_equal 42, result
  end
  
  def test_modulo_operation
    assert_equal 1, 10 % 3
    assert_equal 0, 15 % 5
  end
end

The most frequently used assertion methods include assert_equal for value comparison, assert for truthiness testing, and assert_nil for nil checking. Each assertion accepts an optional message parameter that appears in failure output.

class ValidationTest < Test::Unit::TestCase
  def test_string_validation
    username = "john_doe"
    assert username.length > 3, "Username too short"
    assert_match /^[a-z_]+$/, username, "Invalid characters in username"
    assert_not_equal "", username.strip
  end
  
  def test_array_operations
    numbers = [1, 2, 3, 4, 5]
    assert_equal 5, numbers.length
    assert_include numbers, 3
    assert_not_include numbers, 6
  end
end

Setup and teardown methods run before and after each test method, respectively. These hooks handle test preparation and cleanup without affecting other tests in the same class.

class DatabaseTest < Test::Unit::TestCase
  def setup
    @connection = MockDatabase.new
    @connection.connect
  end
  
  def teardown
    @connection.disconnect if @connection
  end
  
  def test_user_creation
    user = @connection.create_user("alice", "alice@example.com")
    assert_equal "alice", user.name
    assert_equal "alice@example.com", user.email
  end
end

Running tests happens through the command line using ruby test_file.rb or through the test runner with ruby -Itest test/test_calculator.rb. Test::Unit automatically executes all discovered test methods and reports results with pass/fail statistics.

Test organization uses descriptive method names that clearly indicate what behavior each test verifies. Good test names read like specifications, describing the expected outcome under specific conditions.

class ShoppingCartTest < Test::Unit::TestCase
  def test_empty_cart_has_zero_total
    cart = ShoppingCart.new
    assert_equal 0, cart.total
  end
  
  def test_adding_items_increases_total
    cart = ShoppingCart.new
    cart.add_item("apple", 1.50)
    cart.add_item("banana", 0.75)
    assert_equal 2.25, cart.total
  end
  
  def test_removing_items_decreases_total
    cart = ShoppingCart.new
    cart.add_item("apple", 1.50)
    cart.remove_item("apple")
    assert_equal 0, cart.total
  end
end

Advanced Usage

Test::Unit supports sophisticated testing patterns through custom assertion methods, test suites, and advanced configuration options. Custom assertions encapsulate complex validation logic into reusable methods that improve test readability.

class WebServerTest < Test::Unit::TestCase
  def assert_valid_http_response(response)
    assert_match /^HTTP\/\d\.\d \d{3}/, response.status_line
    assert response.headers.is_a?(Hash)
    assert response.body.is_a?(String)
  end
  
  def assert_json_structure(json_string, expected_keys)
    parsed = JSON.parse(json_string)
    expected_keys.each do |key|
      assert parsed.key?(key), "Missing key: #{key}"
    end
  end
  
  def test_api_endpoint_response
    response = WebServer.get("/api/users")
    assert_valid_http_response(response)
    assert_json_structure(response.body, ["users", "total_count", "page"])
  end
end

Test suites aggregate multiple test classes into organized collections that run together. The Test::Unit::TestSuite class manages test execution order and provides centralized result reporting across multiple test files.

require 'test/unit'

class AllModelTests
  def self.suite
    suite = Test::Unit::TestSuite.new("Model Tests")
    suite << UserTest.suite
    suite << ProductTest.suite
    suite << OrderTest.suite
    suite
  end
end

class UserTest < Test::Unit::TestCase
  def test_user_validation
    user = User.new(name: "John", email: "invalid")
    assert_false user.valid?
    assert_include user.errors, :email
  end
end

Parameterized tests handle scenarios requiring multiple input variations without duplicating test logic. Ruby's metaprogramming capabilities enable dynamic test method generation for comprehensive coverage.

class NumberValidationTest < Test::Unit::TestCase
  [
    [1, true],
    [0, false],
    [-1, false],
    [100, true],
    [nil, false]
  ].each do |input, expected|
    define_method("test_positive_number_#{input || 'nil'}") do
      result = NumberValidator.positive?(input)
      assert_equal expected, result, "Failed for input: #{input}"
    end
  end
end

Test contexts organize related tests using nested classes, providing logical grouping and shared setup code. This pattern works especially well for testing different object states or configuration scenarios.

class PaymentProcessorTest < Test::Unit::TestCase
  class WithValidCredentials < Test::Unit::TestCase
    def setup
      @processor = PaymentProcessor.new(api_key: "valid_key")
    end
    
    def test_successful_charge
      result = @processor.charge(amount: 1000, token: "valid_token")
      assert result.success?
      assert_equal 1000, result.amount
    end
    
    def test_invalid_token_handling
      result = @processor.charge(amount: 1000, token: "invalid")
      assert_false result.success?
      assert_equal "Invalid token", result.error_message
    end
  end
  
  class WithInvalidCredentials < Test::Unit::TestCase
    def setup
      @processor = PaymentProcessor.new(api_key: "invalid_key")
    end
    
    def test_authentication_failure
      assert_raises PaymentProcessor::AuthenticationError do
        @processor.charge(amount: 1000, token: "any_token")
      end
    end
  end
end

Test::Unit supports test filtering through command-line options and environment variables. The framework can run specific test methods, test classes, or exclude certain tests based on pattern matching.

# Run specific test method
# ruby test_calculator.rb --name test_addition

# Run tests matching pattern
# ruby test_calculator.rb --name /multiplication/

class CalculatorTest < Test::Unit::TestCase
  def test_addition
    assert_equal 7, Calculator.add(3, 4)
  end
  
  def test_multiplication_positive_numbers
    assert_equal 12, Calculator.multiply(3, 4)
  end
  
  def test_multiplication_with_zero
    assert_equal 0, Calculator.multiply(5, 0)
  end
end

Error Handling & Debugging

Test::Unit distinguishes between assertion failures and unexpected exceptions during test execution. Assertion failures indicate that tested code behaves differently than expected, while exceptions suggest errors in test setup or implementation bugs.

class FileProcessorTest < Test::Unit::TestCase
  def test_file_reading_with_proper_error_handling
    processor = FileProcessor.new
    
    # Test expected exception
    assert_raises FileProcessor::FileNotFoundError do
      processor.read_file("nonexistent.txt")
    end
    
    # Test successful operation
    content = processor.read_file("test_data.txt")
    assert_not_nil content
    assert content.length > 0
  end
  
  def test_malformed_data_handling
    processor = FileProcessor.new
    
    # Create test file with malformed data
    File.write("malformed.txt", "invalid::format::data")
    
    assert_raises FileProcessor::ParseError do
      processor.parse_file("malformed.txt")
    end
  ensure
    File.delete("malformed.txt") if File.exist?("malformed.txt")
  end
end

Debugging test failures requires examining assertion messages, stack traces, and test data carefully. Test::Unit provides detailed failure output including expected versus actual values and the specific line where assertions failed.

class ComplexCalculationTest < Test::Unit::TestCase
  def test_compound_interest_calculation
    principal = 1000.0
    rate = 0.05
    time = 3
    
    expected = principal * (1 + rate) ** time
    actual = FinancialCalculator.compound_interest(principal, rate, time)
    
    # Use delta for floating point comparison
    assert_in_delta expected, actual, 0.01, 
      "Expected #{expected}, got #{actual} for compound interest calculation"
  end
  
  def test_statistical_analysis_with_debugging
    data = [1, 2, 3, 4, 5, 100]  # Note the outlier
    
    mean = StatisticalAnalyzer.mean(data)
    median = StatisticalAnalyzer.median(data)
    
    # Add debugging output for complex calculations
    puts "Data: #{data}"
    puts "Mean: #{mean}, Median: #{median}"
    
    assert mean > median, "Mean should be greater than median with outlier"
    assert_in_delta 19.17, mean, 0.1, "Mean calculation incorrect"
    assert_equal 3.5, median, "Median calculation incorrect"
  end
end

Test isolation failures occur when tests depend on external state or affect each other through shared resources. Proper setup and teardown methods prevent these issues by ensuring clean test environments.

class StatefulServiceTest < Test::Unit::TestCase
  def setup
    @service = StatefulService.new
    @original_config = GlobalConfig.current_settings.dup
    GlobalConfig.reset_to_defaults
  end
  
  def teardown
    GlobalConfig.restore_settings(@original_config)
    @service.cleanup if @service
  end
  
  def test_service_initialization_with_clean_state
    assert_equal :initialized, @service.status
    assert_empty @service.pending_operations
  end
  
  def test_configuration_changes_dont_persist
    GlobalConfig.set_timeout(30)
    @service.configure
    assert_equal 30, @service.timeout
    
    # Verify teardown will reset config for next test
  end
end

Testing Strategies

Test::Unit accommodates various testing approaches including unit tests, integration tests, and acceptance tests. Each approach requires different assertion strategies and test organization patterns.

Unit tests focus on individual methods or small units of functionality in isolation. These tests run quickly and provide immediate feedback about specific behavior changes.

class StringManipulatorTest < Test::Unit::TestCase
  def test_palindrome_detection
    manipulator = StringManipulator.new
    
    assert manipulator.palindrome?("racecar")
    assert manipulator.palindrome?("A man a plan a canal Panama")
    assert_false manipulator.palindrome?("hello")
    assert_false manipulator.palindrome?("")
  end
  
  def test_word_count_accuracy
    manipulator = StringManipulator.new
    text = "The quick brown fox jumps over the lazy dog"
    
    assert_equal 9, manipulator.word_count(text)
    assert_equal 0, manipulator.word_count("")
    assert_equal 1, manipulator.word_count("single")
  end
  
  def test_special_character_handling
    manipulator = StringManipulator.new
    
    assert_equal "hello-world", manipulator.slugify("Hello, World!")
    assert_equal "test-123", manipulator.slugify("Test #123")
    assert_equal "unicode-text", manipulator.slugify("Unicode Tëxt")
  end
end

Integration tests verify that multiple components work together correctly. These tests often require more complex setup and may interact with external systems or dependencies.

class OrderProcessingIntegrationTest < Test::Unit::TestCase
  def setup
    @inventory = InventoryService.new
    @payment_processor = PaymentProcessor.new(test_mode: true)
    @order_manager = OrderManager.new(@inventory, @payment_processor)
    
    # Setup test data
    @inventory.add_product("widget", quantity: 10, price: 29.99)
  end
  
  def test_complete_order_workflow
    # Create order
    order = @order_manager.create_order
    order.add_item("widget", quantity: 2)
    
    # Process payment
    payment_result = order.process_payment(
      card_token: "test_token_valid",
      amount: 59.98
    )
    
    assert payment_result.success?
    assert_equal :paid, order.status
    assert_equal 8, @inventory.quantity_for("widget")
  end
  
  def test_insufficient_inventory_handling
    order = @order_manager.create_order
    order.add_item("widget", quantity: 15)  # More than available
    
    assert_raises OrderManager::InsufficientInventoryError do
      order.process_payment(card_token: "test_token", amount: 100)
    end
    
    assert_equal 10, @inventory.quantity_for("widget")  # Unchanged
  end
end

Mock objects and stubs replace external dependencies during testing, allowing tests to run without network calls, database connections, or file system access. Test::Unit works with any mocking framework or simple stub implementations.

class EmailServiceTest < Test::Unit::TestCase
  def setup
    @email_service = EmailService.new
    @mock_smtp = MockSMTPConnection.new
    @email_service.smtp_connection = @mock_smtp
  end
  
  def test_welcome_email_delivery
    user = User.new(email: "test@example.com", name: "Test User")
    
    @email_service.send_welcome_email(user)
    
    assert_equal 1, @mock_smtp.sent_messages.length
    message = @mock_smtp.sent_messages.first
    
    assert_equal "test@example.com", message.to
    assert_match /Welcome.*Test User/, message.subject
    assert_match /Hello Test User/, message.body
  end
  
  def test_email_delivery_failure_handling
    @mock_smtp.should_fail = true
    user = User.new(email: "test@example.com", name: "Test User")
    
    assert_raises EmailService::DeliveryError do
      @email_service.send_welcome_email(user)
    end
  end
end

Data-driven testing handles multiple input scenarios efficiently using Ruby's iteration capabilities combined with dynamic method definition.

class PasswordValidatorTest < Test::Unit::TestCase
  PASSWORD_TEST_CASES = [
    ["password123", false, "too common"],
    ["P@ssw0rd!", true, "meets all requirements"],
    ["short", false, "too short"],
    ["nouppercase123!", false, "no uppercase"],
    ["NOLOWERCASE123!", false, "no lowercase"],
    ["NoNumbers!", false, "no numbers"],
    ["NoSpecialChars123", false, "no special characters"]
  ].freeze
  
  PASSWORD_TEST_CASES.each_with_index do |(password, expected, description), index|
    define_method("test_password_validation_case_#{index}") do
      validator = PasswordValidator.new
      result = validator.valid?(password)
      assert_equal expected, result, "#{description}: #{password}"
    end
  end
end

Production Patterns

Test::Unit integrates with continuous integration systems through exit codes and output formats. Failed tests cause the Ruby process to exit with non-zero status, triggering build failures in CI environments.

# test/integration/api_health_test.rb
class APIHealthTest < Test::Unit::TestCase
  def setup
    @base_url = ENV['API_BASE_URL'] || 'http://localhost:3000'
    @api_client = HTTPClient.new
  end
  
  def test_database_connectivity
    response = @api_client.get("#{@base_url}/health/database")
    assert_equal 200, response.status
    
    health_data = JSON.parse(response.body)
    assert_equal "ok", health_data["status"]
    assert health_data["response_time_ms"] < 100
  end
  
  def test_external_service_dependencies
    response = @api_client.get("#{@base_url}/health/services")
    assert_equal 200, response.status
    
    services = JSON.parse(response.body)
    %w[payment_gateway email_service user_auth].each do |service|
      assert_equal "operational", services[service]["status"],
        "Service #{service} not operational"
    end
  end
  
  def test_performance_benchmarks
    start_time = Time.now
    response = @api_client.get("#{@base_url}/api/users?limit=100")
    end_time = Time.now
    
    assert_equal 200, response.status
    assert (end_time - start_time) < 2.0, "API response too slow"
    
    users = JSON.parse(response.body)
    assert users["users"].length <= 100
  end
end

Test configuration for production environments uses environment variables and configuration files to adapt test behavior across different deployment stages.

class ConfigurableTest < Test::Unit::TestCase
  def setup
    @config = TestConfiguration.new(
      database_url: ENV['TEST_DATABASE_URL'],
      api_timeout: ENV.fetch('API_TIMEOUT', '5').to_i,
      mock_external_services: ENV['MOCK_SERVICES'] == 'true'
    )
  end
  
  def test_environment_specific_behavior
    if @config.mock_external_services?
      # Use mocked services for fast, reliable tests
      service = MockExternalService.new
    else
      # Test against real services in staging/integration environments
      service = ExternalService.new(@config.api_credentials)
    end
    
    result = service.fetch_user_data(user_id: 123)
    assert_not_nil result
    assert result.key?("user_id")
    assert_equal 123, result["user_id"]
  end
end

Large test suites benefit from parallel execution and selective test running. Test::Unit supports running subsets of tests based on file patterns, test names, or custom tags.

# Rakefile for test automation
require 'rake/testtask'

Rake::TestTask.new(:unit) do |t|
  t.libs << "test"
  t.pattern = "test/unit/**/*_test.rb"
  t.warning = false
end

Rake::TestTask.new(:integration) do |t|
  t.libs << "test"
  t.pattern = "test/integration/**/*_test.rb"
  t.warning = false
end

Rake::TestTask.new(:slow) do |t|
  t.libs << "test"
  t.pattern = "test/**/*_slow_test.rb"
  t.warning = false
end

# Custom test runner with filtering
class CustomTestRunner
  def self.run_performance_tests
    test_files = Dir['test/**/*_performance_test.rb']
    test_files.each { |file| require_relative file }
  end
end

Test reporting and metrics collection provide insights into test suite health and performance trends. Custom test result formatters generate reports for stakeholder communication and quality tracking.

class MetricsCollectingTest < Test::Unit::TestCase
  def setup
    @start_time = Time.now
    @initial_memory = `ps -o rss= -p #{Process.pid}`.to_i
  end
  
  def teardown
    end_time = Time.now
    final_memory = `ps -o rss= -p #{Process.pid}`.to_i
    
    duration = end_time - @start_time
    memory_delta = final_memory - @initial_memory
    
    TestMetrics.record(
      test_name: self.method_name,
      duration: duration,
      memory_usage: memory_delta
    )
  end
  
  def test_memory_intensive_operation
    # Test that generates significant memory usage
    large_array = Array.new(100_000) { |i| "String #{i}" * 10 }
    processor = DataProcessor.new
    result = processor.process(large_array)
    
    assert_equal 100_000, result.processed_count
    # Memory usage will be recorded in teardown
  end
end

Reference

Core Classes and Modules

Class/Module Purpose Key Methods
Test::Unit::TestCase Base class for test cases setup, teardown, assertion methods
Test::Unit::TestSuite Container for multiple tests <<, run, size
Test::Unit::TestResult Stores test execution results run_count, failure_count, error_count
Test::Unit::Assertions Assertion method implementations All assert_* methods

Essential Assertion Methods

Method Parameters Returns Description
assert(test, msg=nil) test (Boolean), msg (String) nil Verifies truthiness
assert_equal(exp, act, msg=nil) exp (Object), act (Object), msg (String) nil Tests equality using ==
assert_nil(obj, msg=nil) obj (Object), msg (String) nil Verifies object is nil
assert_not_nil(obj, msg=nil) obj (Object), msg (String) nil Verifies object is not nil
assert_match(pattern, string, msg=nil) pattern (Regexp), string (String), msg (String) nil Tests regex match
assert_no_match(pattern, string, msg=nil) pattern (Regexp), string (String), msg (String) nil Tests regex non-match
assert_include(collection, obj, msg=nil) collection (Enumerable), obj (Object), msg (String) nil Tests collection membership
assert_not_include(collection, obj, msg=nil) collection (Enumerable), obj (Object), msg (String) nil Tests collection non-membership
assert_raises(*exceptions) { block } exceptions (Class), block (Proc) Exception Expects exception from block
assert_nothing_raised { block } block (Proc) nil Expects no exceptions
assert_in_delta(exp, act, delta, msg=nil) exp (Numeric), act (Numeric), delta (Numeric), msg (String) nil Tests floating point equality within range
assert_throw(symbol) { block } symbol (Symbol), block (Proc) Object Tests symbol throwing
assert_instance_of(class, obj, msg=nil) class (Class), obj (Object), msg (String) nil Tests exact class membership
assert_kind_of(class, obj, msg=nil) class (Class), obj (Object), msg (String) nil Tests class inheritance
assert_respond_to(obj, method, msg=nil) obj (Object), method (Symbol/String), msg (String) nil Tests method availability
assert_same(exp, act, msg=nil) exp (Object), act (Object), msg (String) nil Tests object identity using equal?

Negated Assertions

Method Parameters Description
assert_not_equal(exp, act, msg=nil) exp (Object), act (Object), msg (String) Tests inequality
assert_not_same(exp, act, msg=nil) exp (Object), act (Object), msg (String) Tests different object identity
assert_false(test, msg=nil) test (Boolean), msg (String) Verifies falsiness
assert_not_match(pattern, string, msg=nil) pattern (Regexp), string (String), msg (String) Tests regex non-match

Test Lifecycle Methods

Method Timing Purpose
setup Before each test method Initialize test data and dependencies
teardown After each test method Clean up resources and reset state
startup Before all tests in class One-time expensive setup
shutdown After all tests in class One-time cleanup

Command Line Options

Option Effect
--name PATTERN Run tests matching pattern
--testcase CLASSNAME Run specific test class
--verbose Show detailed output
--progress Display progress indicators
--runner RUNNER Use specific test runner

Exception Classes

Exception Raised When
Test::Unit::AssertionFailedError Assertion fails
Test::Unit::Skipped Test explicitly skipped
Test::Unit::Omitted Test marked as omitted
Test::Unit::Pending Test marked as pending

Test Suite Organization

# Standard test file structure
test/
├── unit/
│   ├── models/
│   │   ├── user_test.rb
│   │   └── product_test.rb
│   └── services/
│       └── email_service_test.rb
├── integration/
│   ├── api_test.rb
│   └── workflow_test.rb
└── test_helper.rb

Environment Variables

Variable Default Purpose
TESTOPTS "" Additional test runner options
TEST All files Specific test file to run
TESTDIR test/ Test directory location

Test Data Management

# test/test_helper.rb
require 'test/unit'

class Test::Unit::TestCase
  def load_fixture(filename)
    File.read(File.join(File.dirname(__FILE__), 'fixtures', filename))
  end
  
  def create_temp_file(content)
    file = Tempfile.new('test')
    file.write(content)
    file.close
    file.path
  end
end