コンテンツにスキップ

Ruby/Minitest

出典: フリー教科書『ウィキブックス(Wikibooks)』

Minitestは、Ruby向けの軽量なテストフレームワークです。Rubyの標準ライブラリに含まれており、Rubyのバージョン1.9以降で利用可能です。Minitestは、テスト駆動開発(TDD)や振る舞い駆動開発(BDD)などのソフトウェア開発手法を支援するために使用されます。

Minitestは、シンプルで直感的な構文を提供し、Rubyの組み込み機能との親和性が高いため、学習コストが比較的低いです。また、高速で効率的なテストランナーを備えており、テストスイート全体の実行速度が速いという利点もあります。

Minitestは、次のような主な機能を提供しています:

  1. テストケースの定義: テストケースは、テスト対象のコードの特定の振る舞いや機能をテストするための単位です。
  2. アサーション: アサーションを使用して、期待される結果を確認します。たとえば、特定の条件が真であることを確認する assert メソッドがあります。
  3. テストランナー: テストランナーは、定義されたテストケースやテストメソッドを実行し、結果を収集して報告します。
  4. テストフィクスチャ: テストフィクスチャを使用して、テストケースの前後に特定の状態を設定したり解放したりします。これにより、テストの再現性と信頼性が向上します。

Minitestを使用することで、Rubyプログラムの品質や安定性を向上させるための効果的なテストを作成することができます。


  1. インストール: MinitestはRubyの bundled gem であり、Rubyをインストールすると含まれています。
  2. テストファイルの作成: テストを実行するためのファイルを作成します。通常、テストファイルの名前は *_test.rb とします。例えば、calculator_test.rb のような名前が一般的です。
  3. テストケースの作成: テストファイル内で、テストケースを作成します。これは、Minitest::Test クラスを継承するクラスです。テストケース内には、テストメソッドが含まれます。
  4. アサーションの使用: テストケース内で、テストの期待結果をアサーションを使用して記述します。アサーションは、実際の結果が期待される結果と一致していることを確認します。
    calculator_test.rb
    def test_addition
      assert_equal 5, 2 + 3
    end
    
    この場合、2 + 3 の結果が 5 と等しいことを確認しています。
  5. テストの実行: テストファイルを実行して、テストを実行します。通常、以下のコマンドを使用します。
    ruby calculator_test.rb
    
    または、rakerake test を使用してテストを実行することもできます。
    rake test
    
  6. テスト結果の確認: テストの実行が完了すると、テスト結果が表示されます。各テストケースが成功したか、失敗したか、またはエラーが発生したかなどの情報が表示されます。

アサーションメソッド

[編集]

以下は、Minitestで利用可能なアサーションメソッドとヘルパーメソッドです。

アサーションメソッド
メソッド 説明
assert(test, msg = nil) test が真であることを確認します。もし test が偽であれば、オプションのメッセージ msg と共にテストは失敗します。
refute(test, msg = nil) test が偽であることを確認します。もし test が真であれば、オプションのメッセージ msg と共にテストは失敗します。
assert_block(msg = nil) ブロックが真であることを確認します。もしブロックが偽であれば、オプションのメッセージ msg と共にテストは失敗します。
assert_empty(obj, msg = nil) obj が空であることを確認します。もし空でない場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_equal(exp, act, msg = nil) expact が等しいことを確認します。もし等しくない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_equal(exp, act, msg = nil) expact が等しくないことを確認します。もし等しい場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_includes(collection, obj, msg = nil) collectionobj を含むことを確認します。もし含まれていない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_includes(collection, obj, msg = nil) collectionobj を含まないことを確認します。もし含んでいる場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_in_delta(exp, act, delta = 0.001, msg = nil) expactdelta の範囲内で等しいことを確認します。もし等しくない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_in_delta(exp, act, delta = 0.001, msg = nil) expactdelta の範囲内で等しくないことを確認します。もし等しい場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_in_epsilon(a, b, epsilon = 0.001, msg = nil) abepsilon の範囲内で近いことを確認します。もし近くない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_in_epsilon(a, b, epsilon = 0.001, msg = nil) abepsilon の範囲内で近くないことを確認します。もし近い場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_instance_of(cls, obj, msg = nil) objcls のインスタンスであることを確認します。もし objcls のインスタンスでない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_instance_of(cls, obj, msg = nil) objcls のインスタンスでないことを確認します。もし objcls のインスタンスである場合は、オプションのメッセージ msg と共にテストは失敗します。
_assertions 現在のアサーションの数を返します。
_assertions= 現在のアサーションの数を設定します。
assert_kind_of(cls, obj, msg = nil) objcls のインスタンスであることを確認します。もし objcls のインスタンスでない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_kind_of(cls, obj, msg = nil) objcls のインスタンスでないことを確認します。もし objcls のインスタンスである場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_match(exp, act, msg = nil) 正規表現 exp が文字列 act にマッチすることを確認します。もしマッチしない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_match(exp, act, msg = nil) 正規表現 exp が文字列 act にマッチしないことを確認します。もしマッチする場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_nil(obj, msg = nil) objnil であることを確認します。もし objnil でない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_nil(obj, msg = nil) objnil でないことを確認します。もし objnil である場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_operator(obj1, op, obj2, msg = nil) obj1obj2 が演算子 op の関係にあることを確認します。もし関係が成立しない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_operator(obj1, op, obj2, msg = nil) obj1obj2 が演算子 op の関係にないことを確認します。もし関係が成立する場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_output(stdout = nil, stderr = nil) { ... } ブロックが実行された際に、標準出力および標準エラー出力が指定された値に等しいかを確認します。
assert_predicate(obj, meth, msg = nil) obj が述語メソッド meth を満たすことを確認します。もし満たさない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_predicate(obj, meth, msg = nil) obj が述語メソッド meth を満たさないことを確認します。もし満たす場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_respond_to(obj, meth, msg = nil) obj がメソッド meth に応答することを確認します。もし応答しない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_respond_to(obj, meth, msg = nil) obj がメソッド meth に応答しないことを確認します。もし応答する場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_same(exp, act, msg = nil) expact が同じオブジェクトであることを確認します。もし同じオブジェクトでない場合は、オプションのメッセージ msg と共にテストは失敗します。
refute_same(exp, act, msg = nil) expact が同じオブジェクトでないことを確認します。もし同じオブジェクトである場合は、オプションのメッセージ msg と共にテストは失敗します。
assert_send(obj, msg = nil) 指定されたメッセージをオブジェクト obj に送信できることを確認します。
assert_silent(msg = nil) ブロック内で出力が行われないことを確認します。
assert_throws(sym, msg = nil) ブロック内で指定されたシンボル sym が投げられることを確認します。
capture_io 標準出力と標準エラー出力をキャプチャするための補助関数を返します。
capture_subprocess_io 標準出力と標準エラー出力をサブプロセスからキャプチャするための補助関数を返します。
diff(exp, act) expact の差分を表示します。
exception_details(exception) 例外の詳細を表示します。
flunk(msg = nil) テストを失敗させます。
message(msg = nil) テスト失敗時のメッセージを設定します。
mu_pp(obj) obj のマルチライン文字列を返します。
mu_pp_for_diff(obj) obj のマルチライン文字列を返します。マルチライン文字列中の各行はプリフィックス `
pass(msg = nil) テストを成功させます。
skip(msg = nil) テストをスキップします。
assert_raises(*exp, msg = nil) ブロック内で例外が発生することを確認します。もし例外が発生しない場合はテストが失敗します。
assert_throws(sym, msg = nil) ブロック内で指定されたシンボル sym が投げられることを確認します。

これらのアサーションメソッドとヘルパーメソッドを使って、テストコードを記述してコードの動作を確認できます。

assert(test, msg = nil)refute(test, msg = nil)の関係のように、assert_*refute_*では確認する論理が逆になります。

2種類のテストケース

[編集]

MinitestにはTestUnitスタイルのテストとRSpecのようなSpecスタイルのテストの両方を記述できます。これらのスタイルにはいくつかの違いがあります。

TestUnitスタイル

[編集]
  1. 構文: TestUnitスタイルでは、テストクラスを作成し、その中にテストメソッドを定義します。テストメソッドの名前は通常、test_で始まります。
    require 'minitest'
    
    class IntegerArithmeticTest < Minitest::Test
      def test_add
        assert_equal 2, 1 + 1
      end
    
      def test_sub
        assert_equal 0, 1 - 1
      end
    
      def test_mul
        assert_equal 6, 2 * 3
      end
    
      def test_mul
        assert_equal 6, 2 * 3
      end
    
      def test_div
        assert_equal 1, 3 / 2
      end
    
      def test_zerodiv
        assert_raises(ZeroDivisionError) { 3 / 0 }
      end
    
      def test_zerozerodiv
        assert_raises(ZeroDivisionError) { 0 / 0 }
      end
    end
    
    Minitest.run
    
  2. 概念の直接性: TestUnitスタイルは比較的直接的でシンプルです。テストクラスとテストメソッドを作成し、アサーションメソッドを使用して期待値と実際の値を比較します。
  3. テストの構造: テストはクラスとメソッドの階層構造を持ちます。これは、テストの整理や分類に便利です。

Specスタイル

[編集]
  1. 構文: Specスタイルでは、describeitを使用してテストのグループ化と記述を行います。describeはテストのグループを作成し、itは特定のテストケースを定義します。
    # frozen_string_literal: true
    
    require 'minitest/spec'
    
    describe '整数演算' do
      it '加算' do
        expect(1 + 1).must_equal 2
      end
    
      it '減算' do
        expect(1 - 1).must_equal 0
      end
    
      it '乗算' do
        expect(2 * 3).must_equal 6
      end
    
      describe '除法' do
        it '除算' do
          expect(3 / 2).must_equal 1
        end
    
        it 'ゼロ除算' do
          expect { 3 / 0 }.must_raise ZeroDivisionError
        end
    
        it 'ゼロゼロ除算' do
          expect { 0 / 0 }.must_raise ZeroDivisionError
        end
      end
    end
    
    Minitest.run
    
  2. ドメイン特化言語(DSL): SpecスタイルはDSLの特徴を持ち、テストが自然言語に近い形で書かれることがあります。must, wont, should, expectなどのメソッドがアサーションを表します。
  3. 期待される動作の表現: Specスタイルでは、テストが期待される動作をより詳細に表現することが一般的です。これにより、テストコードがより読みやすく、テストの目的が明確になります。

両方のスタイルは使いやすく、プロジェクトやチームの好みに応じて選択できます。一般的には、テストの構造をより詳細に表現する必要がある場合はSpecスタイルが好まれ、シンプルな場合はTestUnitスタイルが選択されることがあります。

TestUnit

[編集]

Minitest/Testは、Minitest gemの中核をなすテストライブラリで、xUnit (JUnit、RUnit) スタイルのテストを書くためのツールです。

基本的な構文

[編集]

Minitest/Testでは、Minitest::Testを継承したテストクラスを作成し、そのクラス内にテストメソッドを定義します。テストメソッド名は、test_で始める必要があります。

require 'minitest/autorun'

class ArrayTest < Minitest::Test
  def test_reverse
    array = [1, 2, 3]
    reversed = array.reverse
    assert_equal [3, 2, 1], reversed
  end
end

この例では、ArrayTestクラスを定義し、そこにtest_reverseというテストメソッドを書いています。assert_equalメソッドを使って、実際の結果と期待される結果を比較しています。

アサーション

[編集]

Minitest/Testには、さまざまなアサーションメソッドが用意されています。一般的に使われるものは以下のとおりです。

  • assert(test) : 条件testがtrueであることをアサート
  • refute(test) : 条件testがfalseであることをアサート
  • assert_equal(exp, act) : expactが等しいことをアサート
  • refute_equal(exp, act) : expactが等しくないことをアサート
  • assert_nil(obj) : objがnilであることをアサート
  • refute_nil(obj) : objがnilでないことをアサート
  • assert_raises(Exception) { ... } : ブロックがExceptionを発生させることをアサート
  • assert_output(exp) { ... } : ブロックの出力がexpに一致することをアサート

その他にも多くのアサーションメソッドが用意されており、オブジェクトの種類やパターンマッチなど、さまざまなケースをカバーできます。

セットアップとティアダウン

[編集]

テストの前後で実行したい処理がある場合は、setupメソッドとteardownメソッドをオーバーライドして記述します。

require 'minitest/autorun'

class DatabaseTest < Minitest::Test
  def setup
    @db = Database.new
  end

  def teardown
    @db.close
  end

  def test_query
    result = @db.query('SELECT * FROM users')
    assert_equal 3, result.count
  end
end

この例では、各テストケースの前にDatabaseオブジェクトをインスタンス化し、テストケースの後にデータベース接続を閉じています。setupteardownを適切に使うことで、テストの再現性と信頼性が高まります。

テストの実行

[編集]

Minitestにはテストランナーが組み込まれているので、テストスクリプトを単に実行するだけでテストを実行できます。

# my_test.rb
require 'minitest/autorun'

class MyTest < Minitest::Test
  # ...
end
テストの実行
$ ruby my_test.rb

また、rake(Rakefile経由)、ruby -Ilib:test(ロードパスを追加)などの方法でもテストを実行できます。

まとめ

[編集]

Minitest/Testは、Minitest gemに付属する本来のテストライブラリです。xUnitスタイルの記述でテストを書くことができ、アサーションメソッドやセットアップ/ティアダウンメソッドなどを活用してテストを作成します。小規模から大規模まで幅広いプロジェクトで使われており、信頼性の高いテストを書くことができます。Railsなどのフレームワークのテストでも利用されています。

GCD

[編集]
gcd.rb
# 最大公約数を計算するメソッド
def gcd(m, n) = n.zero? ? m : gcd(n, m % n)

require 'minitest/autorun'

class TestGCD < Minitest::Test
  def test_gcd_with_coprime_numbers
    assert_equal 1, gcd(3, 7)
    assert_equal 1, gcd(10, 21)
    assert_equal 1, gcd(8, 13)
  end
  
  def test_gcd_with_non_coprime_numbers
    assert_equal 2, gcd(4, 6)
    assert_equal 3, gcd(15, 9)
    assert_equal 6, gcd(24, 18)
  end
  
  def test_gcd_with_same_numbers
    assert_equal 5, gcd(5, 5)
    assert_equal 10, gcd(10, 10)
    assert_equal 17, gcd(17, 17)
  end
  
  def test_gcd_with_one_zero
    assert_equal 5, gcd(5, 0)
    assert_equal 10, gcd(0, 10)
    assert_equal 17, gcd(17, 0)
    assert_equal 1, gcd(0, 1)
  end
  
  def test_gcd_with_both_zero
    assert_equal 0, gcd(0, 0)
  end

  def test_gcd_with_large_number
    assert_equal 3, gcd(2**99+1, 2**199+1)
  end
end

最初に、最大公約数を再帰的に計算するためのメソッドを定義しています。 ユークリッドの互除法を使いました。 再帰的な呼び出しを行い、nが0になるまでmとnの最大公約数を求めます。 Rubyの新しい構文である "def method = expression" を使用して、1行でメソッドを定義しています。

次に、Minitestのテストケースが定義されています。 TestGCD クラスは Minitest::Test を継承しており、各テストメソッドで最大公約数メソッドを呼び出して、その結果が期待通りであることを検証します。

各テストメソッド内では assert_equal を使用して、期待される値と実際の値を比較しています。たとえば、assert_equal 1, gcd(3, 7) は、3と7の最大公約数が1であることを検証しています。

このようにして、再帰を利用して最大公約数を計算し、Minitestを使用してテストすることができます。

テストケースの継承

[編集]
二分木クラスを定義
binarytree.rb
# frozen_string_literal: true

# 二分木クラス
class BinaryTree
  include Enumerable # Enumerableモジュールを含める

  # 二分木のノード
  TreeNode = Struct.new(:value, :left, :right)
  def self.new_node(*args) = TreeNode.new(*args)

  # 新しいツリーを作成
  def initialize(*_args)
    @root = nil
  end

  attr_accessor :root

  def height(node = @root) = node.nil? ? 0 : 1 + [height(node.left), height(node.right)].max

  def search(key, _node = @root)
    raise TypeError, "Invalid value: #{key.inspect}" unless key.respond_to?(:<)
    raise TypeError, "Invalid value: #{key.inspect}" if key.is_a?(Numeric) && !key.finite?

    any? { |i| i == key }
  end

  # 中間順序(inorder)で木を走査し、各ノードの値をブロックに渡します。
  def each(node = @root, &block)
    return to_enum(__method__, node) unless block

    def core(node, &block)
      return if node.nil?

      core(node.left, &block)
      yield node.value
      core(node.right, &block)
    end
    core(node, &block)
    self
  end

  # ツリーの文字列表現を返します。
  #
  # Returns ツリーを表す文字列。
  def to_s =  "(#{to_a.join ' '})"

  # ツリーのデバッグ用表現を返します。
  #
  # Returns デバッグ用表現を表す文字列。
  def inspect ="#{self.class}(#{to_a.join ', '})"
end

require 'minitest'

class BinaryTreeTest < Minitest::Test
  def initialize(*args)
    super(*args)
    @target_class = BinaryTree
  end

  def setup
    @tree = @target_class.new
  end

  def test_initialization
    assert_nil @tree.root
  end

  def test_to_s
    assert_equal '()', @tree.to_s
  end

  def test_inspect
    assert_equal "#{@target_class}()", @tree.inspect
  end

  def test_set_value
    @tree.root = @tree.class.new_node(:+)
    assert_equal "#{@target_class}(+)", @tree.inspect
  end

  def test_height_with_empty_tree
    assert_equal 0, @tree.height
  end

  def test_height_with_single_node
    @tree.root = BinaryTree.new_node(5)
    assert_equal 1, @tree.height
  end

  # 空の木のテスト
  def test_empty_tree
    assert_equal '()', @tree.to_s
    assert_equal "#{@target_class}()", @tree.inspect
  end

  # 1つのノードしか持たない木のテスト
  def test_single_node_tree
    @tree.root = @tree.class.new_node(:+)
    assert_equal '(+)', @tree.to_s
    assert_equal "#{@target_class}(+)", @tree.inspect
  end

  def test_add_left
    @tree.root = @tree.class.new_node(:+)
    @tree.root.left = @tree.class.new_node(10)
    assert_equal "#{@target_class}(10, +)", @tree.inspect
  end

  def test_height
    assert_equal 0, @tree.height
    @tree.root = @tree.class.new_node(:+)
    assert_equal 1, @tree.height
    @tree.root.left = @tree.class.new_node(10)
    assert_equal 2, @tree.height
    @tree.root.right = @tree.class.new_node(:*)
    assert_equal 2, @tree.height
    @tree.root.right.left = @tree.class.new_node(20)
    assert_equal 3, @tree.height
    @tree.root.right.right = @tree.class.new_node(30)
    assert_equal 3, @tree.height
    assert_equal '(10 + 20 * 30)', @tree.to_s
    assert_equal "#{@target_class}(10, +, 20, *, 30)", @tree.inspect
  end

  def test_each
    @tree.root = @tree.class.new_node(:+)
    @tree.root.left = @tree.class.new_node(10)
    @tree.root.right = @tree.class.new_node(:*)
    @tree.root.right.left = @tree.class.new_node(20)
    @tree.root.right.right = @tree.class.new_node(30)

    expected = '10 + 20 * 30 '
    assert_equal(expected, capture_stdout { @tree.each { |value| print "#{value} " } })
    assert_equal(expected, capture_stdout do
      enum = @tree.each
      enum.each do |value|
        print "#{value} "
      end
    end)
  end

  def test_search_with_existing_value
    @tree.root = @target_class.new_node(5)
    assert @tree.search(5)
  end

  def test_search_with_non_existing_value
    @tree.root = @target_class.new_node(5)
    refute @tree.search(10)
  end

  def test_search_with_invalid_value
    @tree.root = @target_class.new_node(5)
    assert_raises(TypeError) { @tree.search(0.0 / 0.0) }
  end

  if $PROGRAM_NAME == __FILE__
    def make_tree
      @tree.root = @tree.class.new_node(:+)
      @tree.root.left = @tree.class.new_node(10)
      @tree.root.right = @tree.class.new_node(:*)
      @tree.root.right.left = @tree.class.new_node(20)
      @tree.root.right.right = @tree.class.new_node(30)
    end

    def test_search_no_exist
      make_tree
      refute @tree.search(0)
      refute @tree.search(1)
      refute @tree.search(100)
      refute @tree.search(123.456)
      assert_raises(TypeError) { @tree.search([1, 2, 3]) }
      refute @tree.search({ a: 1 })
    end

    def test_search_right
      make_tree
      assert @tree.search(:*)
    end

    def test_search
      make_tree
      assert @tree.search(:+)
      assert @tree.search(10)
      assert @tree.search(20)
      assert @tree.search(30)
      assert @tree.search(30.0)
    end
  end

  private

  # 標準出力をキャプチャして文字列として返す
  def capture_stdout
    original_stdout = $stdout
    $stdout = StringIO.new
    yield
    $stdout.string
  ensure
    $stdout = original_stdout
  end
end

Minitest.run if $PROGRAM_NAME == __FILE__
二分木クラスを継承し二分探索木クラスを定義
binarytree.rb
# frozen_string_literal: true

require_relative 'binarytree'

# 二分探索木クラス
class BinarySearchTree < BinaryTree
  # 新しい二分探索木を作成します。
  #
  # @param args [Array<Object>] 挿入する要素の配列
  # @yield [element] ブロックが与えられた場合、各要素に対してブロックを実行し、その結果を挿入します。
  # @yieldparam element [Object] 要素
  def initialize(*args)
    @root = nil

    case args
    in [Array(*ary)]
      if block_given?
        ary.each { insert yield(_1) }
      else
        ary.each { insert _1 }
      end
    in []
    else
      raise ArgumentError, "#{self.class}#initialize: #{args.inspect}"
    end
  end

  # 二分探索木に新しい値を挿入します。
  #
  # @param value [Object] 挿入する値
  # @return [BinarySearchTree] 自身のインスタンス
  def insert(value, node = @root)
    raise TypeError, "Invalid value: #{value.inspect}" unless value.respond_to?(:<)
    raise TypeError, "Invalid value: #{value.inspect}" if value.is_a?(Numeric) && !value.finite?

    @root = insert_recursive(value, node)
    # @root = insert_iterative(value, node)
    self
  end

  # 指定されたキーを持つ要素が存在するかどうかを返します。
  #
  # @param key [Object] 検索するキー
  # @return [Boolean] 指定されたキーを持つ要素が存在する場合はtrue、それ以外の場合はfalse
  def search(key, node = @root)
    raise TypeError, "Invalid value: #{key.inspect}" unless key.respond_to?(:<)
    raise TypeError, "Invalid value: #{key.inspect}" if key.is_a?(Numeric) && !key.finite?

    search_recursive(key, node)
    # search_iterative(key, node)
  end

  # 指定されたキーを持つ要素を削除します。
  #
  # @param key [Object] 削除する要素のキー
  # @return [BinarySearchTree] 自身のインスタンス
  def delete(key, node = @root)
    raise TypeError, "Invalid value: #{key.inspect}" unless key.respond_to?(:<)
    raise TypeError, "Invalid value: #{key.inspect}" if key.is_a?(Numeric) && !key.finite?

    @root = delete_node(key, node)
    self
  end

  protected

  # 二分探索木に値を再帰的に挿入します。
  #
  # @param node [Node, nil] 現在のノード
  # @param value [Object] 挿入する値
  # @return [Node] 挿入後のノード
  def insert_recursive(value, node)
    return self.class.new_node(value) if node.nil?

    case value <=> node.value
    when -1 then node.left = insert_recursive(value, node.left)
    when 1 then node.right = insert_recursive(value, node.right)
    when 0 # sum value
    else raise TypeError, value.inspect
    end

    node
  end

  def insert_iterative(value, node)
    return Node.new(value) if node.nil?

    prev = nil
    temp = node
    until temp.nil?
      prev = temp
      temp = case value <=> temp.value
             when -1 then temp.left
             when +1 then temp.right
             when 0 then break
             else raise TypeError, value.inspect end
    end

    unless prev.nil?
      case value <=> prev.value
      when -1 then prev.left = Node.new(value)
      when +1 then prev.right = Node.new(value)
      when 0 # break
      else raise TypeError, value.inspect
      end
    end
    node
  end

  def search_recursive(key, node)
    return false if node.nil?

    case node.value <=> key
    when -1 then search_recursive(key, node.left)
    when +1 then search_recursive(key, node.right)
    when 0 then true
    else raise TypeError, "#{self.class}#search_recursive: #{key.inspect}"
    end
  end

  def search_iterative(key, node)
    until node.nil?
      node = case node.value <=> key
             when -1 then node.left
             when +1 then node.right
             when 0 then return true
             else raise TypeError, "#{self.class}#search_iterative: #{key.inspect}"
             end
    end
    false
  end

  def delete_node(key, node)
    return node if node.nil?

    case key <=> node.value
    when -1
      node.left = delete_node(key, node.left)
      return node
    when 1
      node.right = delete_node(key, node.right)
      return node
    when 0 # sum value
    else raise TypeError, value.inspect
    end

    if node.left.nil?
      return node.right
      elif node.right.nil?
      root.left
    else
      succParent = node
      succ = node.right
      while succ.left
        succParent = succ
        succ = succ.left
      end
      if succParent != node
        succParent.left = succ.right
      else
        succParent.right = succ.right
      end

      node.value = succ.value
      node
    end
  end
end

def BinarySearchTree(args) = BinarySearchTree.new(args)

require 'minitest'

## Minitest::Test
class BinarySearchTreeTest < BinaryTreeTest
  def initialize(*args)
    super(*args)
    @target_class = BinarySearchTree
  end

  def setup
    @tree = @target_class.new
  end

  # 配列を使ってコンストラクタをテストします。
  def test_constructor_with_array
    @tree = @target_class.new([7, 5, 8])
    assert_equal '(5 7 8)', @tree.to_s
  end

  # 配列とブロックを使ってコンストラクタをテストします。
  def test_constructor_with_array_with_block
    @tree = @target_class.new([7, 5, 8]) { |i| 2 * i + 1 }
    assert_equal '(11 15 17)', @tree.to_s
    assert_equal "#{@target_class}(11, 15, 17)", @tree.inspect
  end

  # 文字列を使ってコンストラクタをテストします。
  def test_constructor_with_array
    assert_raises(ArgumentError) { _ = BinarySearchTree.new('abc') }
  end

  def test_inspect
    @tree.insert(1)
    assert_equal "#{@target_class}(1)", @tree.inspect
    @tree.insert(3)
    assert_equal "#{@target_class}(1, 3)", @tree.inspect
    @tree.insert(2)
    assert_equal "#{@target_class}(1, 2, 3)", @tree.inspect
    @tree.insert(0)
    assert_equal "#{@target_class}(0, 1, 2, 3)", @tree.inspect
  end

  # insertメソッドが要素をツリーに追加することをテストします。
  def test_insert_adds_element_to_tree
    @tree.insert(1)
    assert_equal '(1)', @tree.to_s
    @tree.insert(2)
    assert_equal '(1 2)', @tree.to_s
    @tree.insert(-1)
    assert_equal '(-1 1 2)', @tree.to_s
    refute @tree.search(0)
    assert @tree.search(1)
    begin
      assert_equal 'NaN', @tree.search(0.0 / 0.0)
    rescue StandardError
      'NaN'
    end
    @tree.delete 1
    assert_equal '(-1 2)', @tree.to_s
    @tree.delete 0
    assert_equal '(-1 2)', @tree.to_s
    @tree.delete(-2)
    assert_equal '(-1 2)', @tree.to_s
    @tree.delete 3
    assert_equal '(-1 2)', @tree.to_s
    @tree.delete(-1)
    assert_equal '(2)', @tree.to_s
    @tree.delete 2
    assert_equal '()', @tree.to_s
  end

  def test_height
    [10, 5, 15, 3, 7, 12, 18].each { @tree.insert _1 }
    assert_equal 3, @tree.height
    @tree.insert 2
    assert_equal 4, @tree.height
    @tree.insert 1
    assert_equal 5, @tree.height
    @tree.insert 0
    assert_equal 6, @tree.height
    @tree.insert(-1)
    assert_equal 7, @tree.height
    @tree.insert(-2)
    assert_equal 8, @tree.height
    assert_equal "#{@target_class}(-2, -1, 0, 1, 2, 3, 5, 7, 10, 12, 15, 18)", @tree.inspect
  end

  def test_each
    [10, 5, 15, 3, 7, 12, 18].each { @tree.insert _1 }
    expected_output = '3 5 7 10 12 15 18 '
    @tree.insert 10
    assert_equal(expected_output, capture_stdout { @tree.each { |value| print "#{value} " } })
    assert_equal(expected_output, capture_stdout do
      enum = @tree.each
      enum.each do |value|
        print "#{value} "
      end
    end)
  end

  # 二分探索木の要素を削除するテスト
  def test_delete_removes_element_from_tree
    @tree.insert(5)
    @tree.insert(3)
    @tree.insert(7)
    @tree.insert(2)
    @tree.insert(4)
    @tree.insert(6)
    @tree.insert(8)

    # 二分探索木の要素を削除する
    @tree.delete(3)

    # 期待される結果: (2 4 5 6 7 8)
    assert_equal '(2 4 5 6 7 8)', @tree.to_s
  end

  # 存在しない要素を削除するテスト
  def test_delete_non_existent_element
    @tree.insert(5)
    @tree.insert(3)
    @tree.insert(7)

    # 存在しない要素を削除する
    @tree.delete(10)

    # 期待される結果: (3 5 7)
    assert_equal '(3 5 7)', @tree.to_s
  end

  def test_search; end

  # NaNを挿入すると例外が発生しすることを確認するテスト
  def test_inserting_nan_does_raise_exception
    assert_raises(TypeError) { @tree.insert(Float::NAN) }
  end

  def test_serching_nan_does_raise_exception
    assert_raises(TypeError) { @tree.search(Float::NAN) }
  end

  def test_deleting_nan_does_raise_exception
    assert_raises(TypeError) { @tree.delete(Float::NAN) }
  end

  def test_inserting_inf_does_raise_exception
    assert_raises(TypeError) { @tree.insert(Float::INFINITY) }
  end

  def test_serching_inf_does_raise_exception
    assert_raises(TypeError) { @tree.search(Float::INFINITY) }
  end

  def test_deleting_nil_does_raise_exception
    assert_raises(TypeError) { @tree.delete(Float::INFINITY) }
  end

  def test_inserting_nil_does_raise_exception
    assert_raises(TypeError) { @tree.insert(nil) }
  end

  def test_serching_nil_does_raise_exception
    assert_raises(TypeError) { @tree.search(nil) }
  end

  def test_deleting_inf_does_raise_exception
    assert_raises(TypeError) { @tree.delete(nil) }
  end

  def test_inserting_string_does_not_raise_exception
    assert_silent { @tree.insert('abc') }
  end

  def test_serching_string_does_not_raise_exception
    assert_silent { @tree.search('abc') }
  end

  def test_deleting_string_does_not_raise_exception
    assert_silent { @tree.delete('abc') }
  end

  def test_insert_type_missmatch
    assert_raises(TypeError) { @tree.insert('abc').insert(1) }
  end

  def test_insert_zero_and_Zero
    assert_silent { @tree.insert(0).insert(0.0) }
    assert_equal '(0)', @tree.to_s
  end

  def test_insert_strings
    assert_silent { @tree.insert('abc').insert('ab').insert('abs') }
    assert_equal '(ab abc abs)', @tree.to_s
  end

  def test_inserting_compilexes
    assert_raises(TypeError) { @tree.insert(Complex(0, 0)) }
  end

  def test_searching_compilexes
    assert_raises(TypeError) { @tree.search(Complex(0, 0)) }
  end

  def test_deleting_compilexes
    assert_raises(TypeError) { @tree.delete(Complex(0, 0)) }
  end

  # 要素がない場合に検索が正しく動作するかをテスト
  def test_search_on_empty_tree
    refute @tree.search(10)
  end

  # 要素が存在しない場合に検索が正しく動作するかをテスト
  def test_search_non_existent_element
    @tree.insert(5)
    @tree.insert(3)
    @tree.insert(7)

    refute @tree.search(10)
  end

  # 巨大な木を作るテスト
  def test_build_large_tree
    srand(19)
    n = 100_000
    @tree = @target_class.new((0...n).to_a.shuffle)
    assert_equal 43, @tree.height
    assert_equal n, @tree.count
  end

  private

  # 標準出力をキャプチャして文字列として返す
  def capture_stdout
    original_stdout = $stdout
    $stdout = StringIO.new
    yield
    $stdout.string
  ensure
    $stdout = original_stdout
  end
end
Minitest.run if $PROGRAM_NAME == __FILE__

Spec

[編集]

Minitest/Specは、Minitest gem にバンドルされている、RSpecスタイルの記述方法を提供するライブラリです。RSpecのような自然言語に近い記述スタイルを使用することで、テストコードの可読性が向上します。

基本的な構文

[編集]

Minitest/Specでは、describeブロックを使ってテストの対象を記述し、その中にitブロックを使って個々のテストケースを記述します。

require 'minitest/spec'

describe 'Array' do
  describe '#reverse' do
    it 'reverses the order of elements' do
      array = [1, 2, 3]
      reversed = array.reverse
      expect(reversed).must_equal [3, 2, 1]
    end
  end
end

この例では、Array#reverseメソッドのテストを記述しています。describe 'Array'ブロックで、テストの対象をArrayクラスとしています。その中のdescribe '#reverse'ブロックでは、#reverseメソッドに関するテストをグループ化しています。最後にitブロック内に、実際のテストケースを記述しています。

アサーション

[編集]

Minitest/Specでは、RSpecスタイルの記述に合わせた独自のアサーションメソッドが提供されています。

expect(actual).must_equal expected
expect(actual).wont_equal unexpected

must_equalを使うとアサーションが成功した場合、wont_equalを使うと失敗した場合に成功するようになっています。同様にBooleanを期待する場合はmust_be :true?wont_be :true?を使用します。

その他にも以下のようなアサーションメソッドが用意されています。

  • must_be_nil / wont_be_nil
  • must_be_instance_of / wont_be_instance_of
  • must_be_kind_of / wont_be_kind_of
  • must_be_empty / wont_be_empty
  • must_include / wont_include
  • must_match / wont_match
  • must_output / wont_output

前提条件(before/after)

[編集]

Minitest/Specでは、テストケースの前後で実行したい処理をbeforeafteraroundブロックで記述できます。

describe 'Something' do
  before do
    # 全てのテストケースの前に実行される
  end

  after do
    # 全てのテストケースの後に実行される
  end

  around do |tests|
    # テストケースの前後で実行される
    # blockを渡す
    tests.call
  end

  it 'does something' do
    # テストケース
  end
end

beforeブロックはテストケース実行前に、afterブロックはテストケース実行後に実行されます。aroundブロックはテストケース実行の前後に実行され、tests.callでテストケース自身を実行します。

これらのブロックは、テストデータのセットアップやテストが終わった後の後始末など、テストケースを実行する前後で行いたい処理を記述するのに便利です。

Minitest/Specを使うことで、RSpecに近い自然言語風の記述でテストを書くことができ、テストコードの可読性が高まります。同時に、Minitestの軽量さや高速な実行も継承しているため、プロジェクトの要件に合わせて使い分けができます。

はい、Minitest/Specではletlet!を使ってインスタンス変数を初期化することができ、とても重要な機能です。

let

[編集]

letを使うと、各テストケースの実行時にインスタンス変数を初期化するロジックを記述できます。初期化のオーバーヘッドを最小限に抑えつつ、DRY(Don't Repeat Yourself)なコードを書くことができます。

require 'minitest/spec'

describe Array do
  let(:array) { [1, 2, 3] }

  describe '#reverse' do
    it 'reverses the order of elements' do
      reversed = array.reverse
      expect(reversed).must_equal [3, 2, 1]
    end
  end
end

この例では、let(:array) { [1, 2, 3] }でインスタンス変数@arrayを初期化するロジックを定義しています。letブロック内のコードは、そのスコープ内の各テストケースが実行される度に評価されます。

つまり、上の例の場合、#reverseメソッドに関するすべてのテストケースで、@array[1, 2, 3]を返すようになります。

let!

[編集]

let!letに似ていますが、テストケースが実行される前に必ず実行されるという点が異なります。オブジェクトの初期化が重たい場合などに便利です。

require 'minitest/spec'

describe Database do
  let!(:db) { Database.new }

  describe '#query' do
    it 'can query the database' do
      result = db.query('SELECT * FROM users')
      expect(result).wont_be_empty
    end
  end
end

この例では、Databaseオブジェクトの初期化が重たい処理だと想定しています。let!(:db) { Database.new }によって、最初のテストケースが実行される前に@dbが初期化されるので、その後に続くすべてのテストケースで同じオブジェクトを使えます。

letlet!を使うことで、DRYなコードを書けるだけでなく、各テストケースでのインスタンス変数の初期化ロジックを明示的に記述できるので、テストコードの可読性が高まります。

GCD

[編集]
gcd-spec.rb
# 最大公約数を計算するメソッド
def gcd(m, n)
  raise ArgumentError, "Argument 1 must be integers" unless m.is_a?(Integer)
  raise ArgumentError, "Argument 2 must be integers" unless n.is_a?(Integer)
  def core(m, n) = n.zero? ? m.abs : gcd(n, m % n) 
  core(m, n)
end

require 'minitest/spec'

describe "gcd" do
  it "returns the greatest common divisor of two numbers" do
    assert_equal 3, gcd(9, 6)
    assert_equal 5, gcd(10, 15)
    assert_equal 1, gcd(7, 5)
  end

  it "returns the greatest common divisor of two numbers when one of the numbers is negative" do
    assert_equal 2, gcd(-6, 8)
    assert_equal 2, gcd(6, -8)
  end

  it "returns the number itself when one of the numbers is zero" do
    assert_equal 6, gcd(6, 0)
    assert_equal 8, gcd(0, 8)
    assert_equal 0, gcd(0, 0)
  end

  it "raises an ArgumentError when non-integer arguments are provided" do
    assert_raises(ArgumentError) { gcd(3.5, 7) }
    assert_raises(ArgumentError) { gcd("hello", 5) }
    assert_raises(ArgumentError) { gcd(10, []) }
  end
end

Minitest.run

二分木

[編集]
binarytree-spec.rb
# frozen_string_literal: true

# 二分木クラス
class BinaryTree
  include Enumerable # Enumerableモジュールを含める

  # 二分木のノード
  TreeNode = Struct.new(:value, :left, :right)
  def self.new_node(*args) = TreeNode.new(*args)

  # 新しいツリーを作成
  def initialize(*_args)
    @root = nil
  end

  attr_accessor :root

  def height(node = @root) = node.nil? ? 0 : 1 + [height(node.left), height(node.right)].max

  def search(key, _node = @root)
    raise TypeError, "Invalid value: #{key.inspect}" unless key.respond_to?(:<)
    raise TypeError, "Invalid value: #{key.inspect}" if key.is_a?(Numeric) && !key.finite?

    any? { |i| i == key }
  end

  # 中間順序(inorder)で木を走査し、各ノードの値をブロックに渡します。
  def each(node = @root, &block)
    return to_enum(__method__, node) unless block

    def core(node, &block)
      return if node.nil?

      core(node.left, &block)
      yield node.value
      core(node.right, &block)
    end
    core(node, &block)
    self
  end

  # ツリーの文字列表現を返します。
  #
  # Returns ツリーを表す文字列。
  def to_s =  "(#{to_a.join ' '})"

  # ツリーのデバッグ用表現を返します。
  #
  # Returns デバッグ用表現を表す文字列。
  def inspect ="#{self.class}(#{to_a.join ', '})"
end

require 'minitest/spec'

describe BinaryTree do
  let(:target_class) { BinaryTree }
  let(:tree) { target_class.new }

  describe 'initialization' do
    it 'must have nil root' do
      expect(tree.root).must_be_nil
    end
  end

  describe '#to_s' do
    it "must return '()'" do
      expect(tree.to_s).must_equal '()'
    end
  end

  describe '#inspect' do
    it 'must return correct inspection string' do
      expect(tree.inspect).must_equal "#{target_class}()"
    end

    it 'must return correct inspection string' do
      tree.root = tree.class.new_node(5)
      expect(tree.inspect).must_equal "#{target_class}(5)"
    end
  end

  describe '#height' do
    it 'must return 0 for empty tree' do
      expect(tree.height).must_equal 0
    end

    it 'must return 1 for single node tree' do
      tree.root = tree.class.new_node(5)
      expect(tree.height).must_equal 1
    end

    it 'must return correct height for multi-node tree' do
      tree.root = tree.class.new_node(:+)
      tree.root.left = tree.class.new_node(10)
      tree.root.right = tree.class.new_node(:*)
      tree.root.right.left = tree.class.new_node(20)
      tree.root.right.right = tree.class.new_node(30)
      expect(tree.height).must_equal 3
    end
  end

  describe '#search' do
    it 'must return true if value exists' do
      tree.root = target_class.new_node(5)
      expect(tree.search(5)).must_equal true
    end

    it 'must return false if value does not exist' do
      tree.root = target_class.new_node(5)
      expect(tree.search(10)).must_equal false
    end

    it 'must raise TypeError for invalid value' do
      tree.root = target_class.new_node(5)
      expect { tree.search(0.0 / 0.0) }.must_raise TypeError
    end
  end

  describe '#each' do
    it 'must iterate over tree nodes in order' do
      tree.root = tree.class.new_node(:+)
      tree.root.left = tree.class.new_node(10)
      tree.root.right = tree.class.new_node(:*)
      tree.root.right.left = tree.class.new_node(20)
      tree.root.right.right = tree.class.new_node(30)
      actual = []
      tree.each { |value| actual << "#{value} " }
      expect(actual.join).must_equal '10 + 20 * 30 '
    end
  end
end

Minitest.run if $PROGRAM_NAME == __FILE__

Bench

[編集]

Minitest/Benchは、Minitestに含まれるベンチマークツールです。コードのパフォーマンス測定やプロファイリングを行う際に役立ちます。

基本的な使い方

[編集]

Minitest/Benchを使うには、minitest/benchmarkを requireして、Minitest::Benchmarkを継承したクラスを定義します。

require 'minitest/benchmark'

class BenchmarkSuite < Minitest::Benchmark
  def bench_array_reverse
    assert_performance_linear 0.9999 do |n|
      n.times do
        (1..1000).to_a.reverse
      end
    end
  end
end
実行結果の例
Run options: --seed 42439

# Running:

bench_array_reverse	 0.000143	 0.000332	 0.003698	 0.037538	 0.344312
.

Finished in 0.424787s, 2.3541 runs/s, 2.3541 assertions/s. 
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

上記の例では、bench_array_reverseメソッドを定義し、assert_performance_linearでベンチマークを行っています。このメソッドは、ブロック内のコードを繰り返し実行し、その実行時間がリニアなスケーリングであることを検証します。

ベンチマークメソッド

[編集]

Minitest/Benchには、以下のようなベンチマークメソッドが用意されています。

  • assert_performance_linear(expected_slope, &block) - ブロックの実行時間が線形スケーリングであることをアサート
  • assert_performance_exponential(expected_slope, &block) - ブロックの実行時間が指数スケーリングであることをアサート
  • assert_performance_constant(&block) - ブロックの実行時間が一定であることをアサート
  • bench_exp(mantissa, largest_exponent, &block) - ブロックを指数的に増加する入力で実行し、結果を出力
  • bench_linear(range_exponent, constant_increment, &block) - ブロックを線形的に増加する入力で実行し、結果を出力

これらのメソッドを使うことで、コードのパフォーマンスを体系的に分析することができます。

拡張機能

[編集]

Minitest/Benchmarkは、ベンチマーク結果の出力フォーマットをカスタマイズすることもできます。独自のフォーマッターを書いて、Minitest::Benchmark.formatterに設定します。

module MyFormatter
  def self.bench(result)
    # 独自の出力フォーマットを記述
    puts "My benchmark: #{result.path}: #{result.data}"
  end
end

Minitest::Benchmark.formatter = MyFormatter

また、GC回数の制御やRubyのインストールパスの変更などの細かい設定も可能です。

まとめ

[編集]

Minitest/Benchmarkは、Rubyコードのベンチマークとパフォーマンス分析を行うためのシンプルで強力なツールです。アサーションベースのベンチマークメソッドを使って、さまざまな入力サイズに対するコードの実行時間を確認し、最悪実行時間の次数を検証できます。フォーマッターをカスタマイズすることで、ベンチマーク結果の出力も自由にコントロールできます。Rubyコードの最適化を行う際に、Minitest/Benchmarkは非常に役立ちます。

Mock

[編集]

Minitest/Mockは、Rubyプログラミング言語向けのモック(Mock)ライブラリです。モックは、テスト駆動開発(TDD)やユニットテストにおいて、依存するコンポーネントを置き換えてテストを行うための仕組みです。具体的には、テスト対象のコードが他のクラスやモジュールと相互作用している場合、その依存関係を模倣(モック化)して、テストをより制御可能にします。

Minitest/Mockは、Minitestフレームワークの一部として提供されており、Minitestを使用してRubyアプリケーションのユニットテストを行う際に、モックオブジェクトを容易に作成および利用できます。

モックを使用することで、テスト対象のコードが他のコードとの連携に問題がある場合でも、それを独立してテストすることが可能になります。また、外部のリソースや環境に依存する場合にも、モックを使ってその依存関係を排除してテストを行うことができます。これにより、より信頼性の高いテストスイートを構築することができます。

Minitest/Mockを使用すると、以下のようなことが可能です:

  1. 依存関係の置き換え: テスト対象のコードが他のクラスやモジュールと連携している場合、それらの依存関係をモックオブジェクトで置き換えることができます。これにより、テストの際に外部の状態に依存することなく、コードの特定の部分の振る舞いを確認できます。
  2. テストの制御: モックオブジェクトを使用することで、テスト中に特定のメソッドが呼び出されたかどうかや、どのような引数で呼び出されたかなどを制御できます。これにより、特定の条件下での振る舞いをシミュレートしてテストを行うことができます。
  3. テストの独立性: モックを使用することで、テスト対象のコードが他のコンポーネントに依存している場合でも、それらのコンポーネントの実際の実装を使用せずにテストを行うことができます。これにより、テストの独立性が高まり、テストスイート全体の信頼性が向上します。

総括すると、Minitest/Mockは、Rubyアプリケーションのユニットテストをより効果的に行うためのツールであり、依存関係の管理やテストの制御、テストの独立性の確保などに役立ちます。

Minitest/Mockを使ったユニットテストの例を示します。

まず、以下のようなクラスがあるとします。

userman.rb
class UserManager
  def initialize(database)
    @database = database
  end

  def create_user(name, email)
    @database.insert(name, email)
  end
end

これをテストするために、データベースへの挿入をモック化してテストします。

userman_test.rb
require 'minitest/spec'
require 'minitest/autorun'
require_relative 'userman'

describe UserManager do
  let(:mock_database) { Minitest::Mock.new }

  before do
    @user_manager = UserManager.new(mock_database)
  end

  describe "#create_user" do
    it "calls insert method on the database" do
      mock_database.expect(:insert, nil, ['John Doe', 'john@example.com'])

      @user_manager.create_user('John Doe', 'john@example.com')

      mock_database.verify
    end
  end
end

このテストでは、UserManagerクラスのcreate_userメソッドがデータベースにユーザーを挿入することを確認しています。しかし、実際のデータベース接続を行う代わりに、MiniTest::Mockを使用してinsertメソッドが呼ばれることをモック化しています。そして、verifyメソッドを呼び出して、モックが期待通りに振る舞っていることを確認しています。

このようにして、Minitest/Mockを使用して、外部の依存関係を置き換えてテストを行うことができます。

Stub

[編集]

Minitestには、Stubとして使用できる機能が含まれています。Stubは、テスト中に特定のメソッド呼び出しに対する返り値や例外を設定するためのものです。MinitestのStub機能を使用することで、テスト中に依存するオブジェクトやメソッドの振る舞いを制御し、テストをより柔軟に行うことができます。

MinitestのStub機能を使って、特定のメソッド呼び出しに対する返り値を設定したり、メソッドが呼び出された際に例外を発生させたりすることができます。これにより、テスト中に実際のコードが依存する外部リソースやモジュールの振る舞いをシミュレートすることができます。

以下は、MinitestでStubを使用してメソッドの振る舞いを制御する例です:

class MyClass
  def self.my_method
    # 何か重要な処理
    return "Hello, world!"
  end
end

require 'minitest/autorun'
require 'minitest/spec'

describe MyClass do
  describe '#my_method' do
    it 'returns stubbed response' do
      MyClass.stub(:my_method, "Stubbed response") do
        expect(MyClass.my_method).must_equal "Stubbed response" 
      end

      # Stubの影響が終わった後には、通常の振る舞いを確認する
      expect(MyClass.my_method).must_equal "Hello, world!"
    end
  end
end

この例では、MyClassmy_methodがテスト対象のメソッドです。テスト中にこのメソッドの振る舞いを制御するために、MyClass.stubを使用して特定の返り値を設定しています。その後、通常のメソッド呼び出しの振る舞いが正しいことを確認します。

これにより、テスト中に外部リソースやモジュールの振る舞いを制御し、テストの安定性や再現性を向上させることができます。