Test::Unit
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
- Test::Unit Official Documentation
- Ruby-Doc Test::Unit
- Test::Unit::TestCase
- Ruby Programming Guide - Unit Testing
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