Test::Unit
Test::Unit
概要
Test::UnitはRuby用のxUnitスタイルのテストフレームワークです。Rubyの標準ライブラリとして長期間提供され、シンプルで直感的なテスト記述を可能にします。テストケースをクラスとして組織化し、セットアップ・ティアダウンメカニズム、豊富なアサーション、自動テスト実行機能を提供します。Ruby 1.9以降はMiniTestに移行されましたが、その設計思想と基本パターンは現代のRubyテストフレームワークの基盤となっています。
詳細
主な特徴
- xUnitアーキテクチャ: クラシックなテストパターンに基づく設計
- シンプルな構造:
Test::Unit::TestCaseを継承した直感的なテスト記述 - セットアップ・ティアダウン: テストフィクスチャの自動管理
- 豊富なアサーション: 包括的なアサーションメソッド群
- 自動テスト実行:
require 'test/unit'による自動実行機能 - 多様なテストランナー: コンソール、GTK+等の複数実行環境
- テスト組織化: テストスイートによる階層的テスト管理
アーキテクチャ
Test::Unitは以下の核心コンポーネントで構成されています:
- TestCase: 個別テストクラスの基底クラス
- Assertions: アサーションメソッドの提供モジュール
- TestSuite: 複数テストケースの集約・実行管理
- TestRunner: テスト実行とレポート生成
- Fixture: テストデータのセットアップ・ティアダウン管理
メリット・デメリット
メリット
- 学習コストの低さ: シンプルで直感的なAPI設計
- 標準統合: Rubyの標準ライブラリとしての安定性
- 豊富なアサーション: 多様なテストシナリオに対応
- 自動実行: ファイル実行だけでテストが開始
- 拡張性: カスタムアサーションとテストランナーの作成可能
- デバッグ支援: 詳細なエラーメッセージとスタックトレース
デメリット
- 古いアーキテクチャ: 現代的なテスト機能の不足
- モック機能なし: 外部依存のモッキングサポートなし
- BDD非対応: 行動駆動開発スタイルの記述不可
- 並列実行: 並列テスト実行機能なし
- 継承強制: TestCaseクラス継承による設計制約
参考ページ
書き方の例
基本的なテストケース
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
セットアップとティアダウン
require 'test/unit'
class BankAccountTest < Test::Unit::TestCase
def setup
# 各テストメソッド実行前に呼ばれる
@account = BankAccount.new(1000) # 初期残高1000円
@transaction_log = []
end
def teardown
# 各テストメソッド実行後に呼ばれる
@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
# 残高不足の場合
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
豊富なアサーション
require 'test/unit'
class AssertionExamplesTest < Test::Unit::TestCase
def test_equality_assertions
# 等価性のテスト
assert_equal "hello", "hello"
assert_not_equal "hello", "world"
# 同一性のテスト
obj = Object.new
assert_same obj, obj
assert_not_same Object.new, Object.new
end
def test_boolean_assertions
assert true
assert_not false
# 真偽値の厳密なテスト
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
# 空の確認
assert_empty []
assert_not_empty array
end
def test_pattern_assertions
# 正規表現マッチ
assert_match /hello/, "hello world"
assert_no_match /goodbye/, "hello world"
end
def test_exception_assertions
# 例外が発生することを期待
assert_raise(ZeroDivisionError) do
1 / 0
end
# 特定のメッセージを持つ例外
exception = assert_raise(ArgumentError) do
raise ArgumentError, "invalid argument"
end
assert_equal "invalid argument", exception.message
# 例外が発生しないことを期待
assert_nothing_raised do
puts "This should not raise"
end
end
def test_comparison_assertions
assert_operator 5, :>, 3
assert_operator "abc", :<, "def"
# 範囲のテスト
assert_in_delta 3.14159, Math::PI, 0.001
end
end
カスタムアサーション
require 'test/unit'
class CustomAssertionTest < Test::Unit::TestCase
# カスタムアサーションメソッドの定義
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
# カスタムアサーションのテスト
def test_custom_assertions
assert_valid_email "[email protected]"
assert_positive 42
assert_between 5, 1, 10
# 失敗例をテスト
assert_raise(Test::Unit::AssertionFailedError) do
assert_valid_email "invalid-email"
end
end
end
テストスイートの組織化
require 'test/unit'
# 個別テストクラス
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
# カスタムテストスイート
class ApplicationTestSuite
def self.suite
suite = Test::Unit::TestSuite.new
suite << UserTest.suite
suite << ProductTest.suite
suite
end
end
# 特定のテストメソッドのみを実行するスイート
class SpecificTestSuite
def self.suite
suite = Test::Unit::TestSuite.new
# 特定のテストメソッドのみ追加
suite << UserTest.new("test_user_creation")
suite << ProductTest.new("test_product_creation")
suite
end
end
データ駆動テスト
require 'test/unit'
class MathOperationsTest < Test::Unit::TestCase
# テストデータの定義
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
エラーハンドリングとデバッグ
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呼び出しのシミュレーション
OpenStruct.new(
status_code: 200,
body: '{"user_id": 123, "name": "John"}',
data: { 'user_id' => 123, 'name' => 'John' }
)
end
end
テスト実行とレポート
# テストファイルの最下部に追加
if __FILE__ == $0
# 自動実行を無効にして手動制御
Test::Unit.setup_argv do |files|
if files.empty?
[__FILE__]
else
files
end
end
# カスタムランナーオプション
Test::Unit::AutoRunner.run(true, '.', [
'--verbose',
'--use-color=true',
'--runner=console'
])
end