Test::Unit

RubyTesting FrameworkxUnitUnit TestingAssertionsSetup

Test::Unit

Overview

Test::Unit is an xUnit-style testing framework for Ruby. It was provided as part of Ruby's standard library for a long time, enabling simple and intuitive test descriptions. It organizes test cases as classes and provides setup/teardown mechanisms, rich assertions, and automatic test execution capabilities. While Ruby 1.9+ moved to MiniTest, its design philosophy and basic patterns form the foundation of modern Ruby testing frameworks.

Details

Key Features

  • xUnit Architecture: Design based on classic testing patterns
  • Simple Structure: Intuitive test description by inheriting from Test::Unit::TestCase
  • Setup/Teardown: Automatic management of test fixtures
  • Rich Assertions: Comprehensive set of assertion methods
  • Automatic Test Execution: Automatic execution via require 'test/unit'
  • Multiple Test Runners: Various execution environments including console and GTK+
  • Test Organization: Hierarchical test management through test suites

Architecture

Test::Unit consists of the following core components:

  • TestCase: Base class for individual test classes
  • Assertions: Module providing assertion methods
  • TestSuite: Aggregation and execution management of multiple test cases
  • TestRunner: Test execution and report generation
  • Fixture: Setup and teardown management for test data

Pros and Cons

Pros

  • Low Learning Curve: Simple and intuitive API design
  • Standard Integration: Stability as part of Ruby's standard library
  • Rich Assertions: Support for diverse testing scenarios
  • Automatic Execution: Tests start with just file execution
  • Extensibility: Ability to create custom assertions and test runners
  • Debug Support: Detailed error messages and stack traces

Cons

  • Legacy Architecture: Lack of modern testing features
  • No Mock Functionality: No support for mocking external dependencies
  • No BDD Support: Cannot write in behavior-driven development style
  • No Parallel Execution: No parallel test execution capability
  • Inheritance Constraint: Design constraints due to required TestCase class inheritance

References

Code Examples

Basic Test Case

require 'test/unit'

class CalculatorTest < Test::Unit::TestCase
  def test_addition
    assert_equal 4, 2 + 2
    assert_equal 0, -2 + 2
    assert_equal -4, -2 + -2
  end
  
  def test_subtraction
    assert_equal 0, 2 - 2
    assert_equal 4, 6 - 2
    assert_equal -1, 2 - 3
  end
  
  def test_multiplication
    assert_equal 4, 2 * 2
    assert_equal 0, 0 * 100
    assert_equal -6, 2 * -3
  end
  
  def test_division
    assert_equal 2, 4 / 2
    assert_equal 0, 0 / 5
    assert_equal 3, 9 / 3
  end
end

Setup and Teardown

require 'test/unit'

class BankAccountTest < Test::Unit::TestCase
  def setup
    # Called before each test method execution
    @account = BankAccount.new(1000) # Initial balance of 1000 yen
    @transaction_log = []
  end
  
  def teardown
    # Called after each test method execution
    @account = nil
    @transaction_log.clear
  end
  
  def test_initial_balance
    assert_equal 1000, @account.balance
  end
  
  def test_deposit
    @account.deposit(500)
    assert_equal 1500, @account.balance
    
    @account.deposit(0)
    assert_equal 1500, @account.balance
  end
  
  def test_withdraw
    @account.withdraw(300)
    assert_equal 700, @account.balance
    
    # Case of insufficient funds
    assert_raise(InsufficientFundsError) do
      @account.withdraw(800)
    end
  end
  
  def test_transfer
    target_account = BankAccount.new(500)
    
    @account.transfer(200, target_account)
    assert_equal 800, @account.balance
    assert_equal 700, target_account.balance
  end
end

Rich Assertions

require 'test/unit'

class AssertionExamplesTest < Test::Unit::TestCase
  def test_equality_assertions
    # Equality testing
    assert_equal "hello", "hello"
    assert_not_equal "hello", "world"
    
    # Identity testing
    obj = Object.new
    assert_same obj, obj
    assert_not_same Object.new, Object.new
  end
  
  def test_boolean_assertions
    assert true
    assert_not false
    
    # Strict boolean testing
    assert_true true
    assert_false false
  end
  
  def test_nil_assertions
    assert_nil nil
    assert_not_nil "not nil"
  end
  
  def test_type_assertions
    assert_instance_of String, "hello"
    assert_kind_of Numeric, 42
    assert_kind_of Numeric, 3.14
  end
  
  def test_collection_assertions
    array = [1, 2, 3, 4, 5]
    
    assert_include array, 3
    assert_not_include array, 6
    
    # Empty checking
    assert_empty []
    assert_not_empty array
  end
  
  def test_pattern_assertions
    # Regular expression matching
    assert_match /hello/, "hello world"
    assert_no_match /goodbye/, "hello world"
  end
  
  def test_exception_assertions
    # Expect an exception to be raised
    assert_raise(ZeroDivisionError) do
      1 / 0
    end
    
    # Exception with specific message
    exception = assert_raise(ArgumentError) do
      raise ArgumentError, "invalid argument"
    end
    assert_equal "invalid argument", exception.message
    
    # Expect no exception to be raised
    assert_nothing_raised do
      puts "This should not raise"
    end
  end
  
  def test_comparison_assertions
    assert_operator 5, :>, 3
    assert_operator "abc", :<, "def"
    
    # Range testing
    assert_in_delta 3.14159, Math::PI, 0.001
  end
end

Custom Assertions

require 'test/unit'

class CustomAssertionTest < Test::Unit::TestCase
  # Custom assertion method definitions
  def assert_valid_email(email, message = nil)
    full_message = build_message(message, 
      "Expected <?> to be a valid email address", email)
    
    email_regex = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    assert_block(full_message) { email_regex.match?(email) }
  end
  
  def assert_positive(number, message = nil)
    full_message = build_message(message,
      "Expected <?> to be positive", number)
    assert_block(full_message) { number > 0 }
  end
  
  def assert_between(value, min, max, message = nil)
    full_message = build_message(message,
      "Expected <?> to be between <?> and <?>", value, min, max)
    assert_block(full_message) { value >= min && value <= max }
  end
  
  # Testing custom assertions
  def test_custom_assertions
    assert_valid_email "[email protected]"
    assert_positive 42
    assert_between 5, 1, 10
    
    # Test failure cases
    assert_raise(Test::Unit::AssertionFailedError) do
      assert_valid_email "invalid-email"
    end
  end
end

Test Suite Organization

require 'test/unit'

# Individual test classes
class UserTest < Test::Unit::TestCase
  def test_user_creation
    user = User.new("John", "[email protected]")
    assert_equal "John", user.name
    assert_equal "[email protected]", user.email
  end
end

class ProductTest < Test::Unit::TestCase
  def test_product_creation
    product = Product.new("Laptop", 1000)
    assert_equal "Laptop", product.name
    assert_equal 1000, product.price
  end
end

# Custom test suite
class ApplicationTestSuite
  def self.suite
    suite = Test::Unit::TestSuite.new
    suite << UserTest.suite
    suite << ProductTest.suite
    suite
  end
end

# Suite with only specific test methods
class SpecificTestSuite
  def self.suite
    suite = Test::Unit::TestSuite.new
    
    # Add only specific test methods
    suite << UserTest.new("test_user_creation")
    suite << ProductTest.new("test_product_creation")
    
    suite
  end
end

Data-Driven Testing

require 'test/unit'

class MathOperationsTest < Test::Unit::TestCase
  # Test data definitions
  ADDITION_TEST_DATA = [
    [1, 2, 3],
    [0, 0, 0],
    [-1, 1, 0],
    [100, -50, 50],
    [0.1, 0.2, 0.3]
  ]
  
  MULTIPLICATION_TEST_DATA = [
    [2, 3, 6],
    [0, 100, 0],
    [-2, 3, -6],
    [0.5, 4, 2.0]
  ]
  
  def test_addition_data_driven
    ADDITION_TEST_DATA.each do |a, b, expected|
      result = a + b
      assert_in_delta expected, result, 0.001, 
        "Failed for #{a} + #{b}, expected #{expected} but got #{result}"
    end
  end
  
  def test_multiplication_data_driven
    MULTIPLICATION_TEST_DATA.each do |a, b, expected|
      result = a * b
      assert_in_delta expected, result, 0.001,
        "Failed for #{a} * #{b}, expected #{expected} but got #{result}"
    end
  end
end

Error Handling and Debugging

require 'test/unit'

class ErrorHandlingTest < Test::Unit::TestCase
  def test_with_detailed_error_message
    user_data = { name: "", email: "invalid" }
    
    begin
      user = User.new(user_data[:name], user_data[:email])
    rescue => e
      flunk "User creation failed with data #{user_data}: #{e.message}"
    end
    
    assert_not_nil user
  end
  
  def test_multiple_conditions_with_context
    values = [1, 2, 3, 4, 5]
    
    values.each_with_index do |value, index|
      assert value > 0, "Value at index #{index} should be positive, got #{value}"
      assert value <= 5, "Value at index #{index} should be <= 5, got #{value}"
    end
  end
  
  def test_complex_assertion_with_explanation
    response = simulate_api_call
    
    assert_equal 200, response.status_code,
      "Expected successful response but got #{response.status_code}: #{response.body}"
    
    assert_not_nil response.data,
      "Response data should not be nil. Full response: #{response.inspect}"
    
    assert response.data.key?('user_id'),
      "Response should contain user_id. Available keys: #{response.data.keys}"
  end
  
  private
  
  def simulate_api_call
    # API call simulation
    OpenStruct.new(
      status_code: 200,
      body: '{"user_id": 123, "name": "John"}',
      data: { 'user_id' => 123, 'name' => 'John' }
    )
  end
end

Test Execution and Reporting

# Add to the bottom of test file
if __FILE__ == $0
  # Disable automatic execution for manual control
  Test::Unit.setup_argv do |files|
    if files.empty?
      [__FILE__]
    else
      files
    end
  end
  
  # Custom runner options
  Test::Unit::AutoRunner.run(true, '.', [
    '--verbose',
    '--use-color=true',
    '--runner=console'
  ])
end