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