Test::Unit

RubyテストフレームワークxUnit単体テストアサーションセットアップ

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