Ruby/Minitest
Minitestは、Ruby向けの軽量なテストフレームワークです。Rubyの標準ライブラリに含まれており、Rubyのバージョン1.9以降で利用可能です。Minitestは、テスト駆動開発(TDD)や振る舞い駆動開発(BDD)などのソフトウェア開発手法を支援するために使用されます。
Minitestは、シンプルで直感的な構文を提供し、Rubyの組み込み機能との親和性が高いため、学習コストが比較的低いです。また、高速で効率的なテストランナーを備えており、テストスイート全体の実行速度が速いという利点もあります。
Minitestは、次のような主な機能を提供しています:
- テストケースの定義: テストケースは、テスト対象のコードの特定の振る舞いや機能をテストするための単位です。
- アサーション: アサーションを使用して、期待される結果を確認します。たとえば、特定の条件が真であることを確認する
assert
メソッドがあります。 - テストランナー: テストランナーは、定義されたテストケースやテストメソッドを実行し、結果を収集して報告します。
- テストフィクスチャ: テストフィクスチャを使用して、テストケースの前後に特定の状態を設定したり解放したりします。これにより、テストの再現性と信頼性が向上します。
Minitestを使用することで、Rubyプログラムの品質や安定性を向上させるための効果的なテストを作成することができます。
- インストール: MinitestはRubyの bundled gem であり、Rubyをインストールすると含まれています。
- テストファイルの作成: テストを実行するためのファイルを作成します。通常、テストファイルの名前は
*_test.rb
とします。例えば、calculator_test.rb
のような名前が一般的です。 - テストケースの作成: テストファイル内で、テストケースを作成します。これは、
Minitest::Test
クラスを継承するクラスです。テストケース内には、テストメソッドが含まれます。 - アサーションの使用: テストケース内で、テストの期待結果をアサーションを使用して記述します。アサーションは、実際の結果が期待される結果と一致していることを確認します。
- calculator_test.rb
def test_addition assert_equal 5, 2 + 3 end
- この場合、
2 + 3
の結果が5
と等しいことを確認しています。
- テストの実行: テストファイルを実行して、テストを実行します。通常、以下のコマンドを使用します。
ruby calculator_test.rb
- または、
rake
やrake test
を使用してテストを実行することもできます。 rake test
- テスト結果の確認: テストの実行が完了すると、テスト結果が表示されます。各テストケースが成功したか、失敗したか、またはエラーが発生したかなどの情報が表示されます。
アサーションメソッド
編集以下は、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)
exp
とact
が等しいことを確認します。もし等しくない場合は、オプションのメッセージmsg
と共にテストは失敗します。refute_equal(exp, act, msg = nil)
exp
とact
が等しくないことを確認します。もし等しい場合は、オプションのメッセージmsg
と共にテストは失敗します。assert_includes(collection, obj, msg = nil)
collection
がobj
を含むことを確認します。もし含まれていない場合は、オプションのメッセージmsg
と共にテストは失敗します。refute_includes(collection, obj, msg = nil)
collection
がobj
を含まないことを確認します。もし含んでいる場合は、オプションのメッセージmsg
と共にテストは失敗します。assert_in_delta(exp, act, delta = 0.001, msg = nil)
exp
とact
がdelta
の範囲内で等しいことを確認します。もし等しくない場合は、オプションのメッセージmsg
と共にテストは失敗します。refute_in_delta(exp, act, delta = 0.001, msg = nil)
exp
とact
がdelta
の範囲内で等しくないことを確認します。もし等しい場合は、オプションのメッセージmsg
と共にテストは失敗します。assert_in_epsilon(a, b, epsilon = 0.001, msg = nil)
a
とb
がepsilon
の範囲内で近いことを確認します。もし近くない場合は、オプションのメッセージmsg
と共にテストは失敗します。refute_in_epsilon(a, b, epsilon = 0.001, msg = nil)
a
とb
がepsilon
の範囲内で近くないことを確認します。もし近い場合は、オプションのメッセージmsg
と共にテストは失敗します。assert_instance_of(cls, obj, msg = nil)
obj
がcls
のインスタンスであることを確認します。もしobj
がcls
のインスタンスでない場合は、オプションのメッセージmsg
と共にテストは失敗します。refute_instance_of(cls, obj, msg = nil)
obj
がcls
のインスタンスでないことを確認します。もしobj
がcls
のインスタンスである場合は、オプションのメッセージmsg
と共にテストは失敗します。_assertions
現在のアサーションの数を返します。 _assertions=
現在のアサーションの数を設定します。 assert_kind_of(cls, obj, msg = nil)
obj
がcls
のインスタンスであることを確認します。もしobj
がcls
のインスタンスでない場合は、オプションのメッセージmsg
と共にテストは失敗します。refute_kind_of(cls, obj, msg = nil)
obj
がcls
のインスタンスでないことを確認します。もしobj
がcls
のインスタンスである場合は、オプションのメッセージmsg
と共にテストは失敗します。assert_match(exp, act, msg = nil)
正規表現 exp
が文字列act
にマッチすることを確認します。もしマッチしない場合は、オプションのメッセージmsg
と共にテストは失敗します。refute_match(exp, act, msg = nil)
正規表現 exp
が文字列act
にマッチしないことを確認します。もしマッチする場合は、オプションのメッセージmsg
と共にテストは失敗します。assert_nil(obj, msg = nil)
obj
がnil
であることを確認します。もしobj
がnil
でない場合は、オプションのメッセージmsg
と共にテストは失敗します。refute_nil(obj, msg = nil)
obj
がnil
でないことを確認します。もしobj
がnil
である場合は、オプションのメッセージmsg
と共にテストは失敗します。assert_operator(obj1, op, obj2, msg = nil)
obj1
とobj2
が演算子op
の関係にあることを確認します。もし関係が成立しない場合は、オプションのメッセージmsg
と共にテストは失敗します。refute_operator(obj1, op, obj2, msg = nil)
obj1
とobj2
が演算子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)
exp
とact
が同じオブジェクトであることを確認します。もし同じオブジェクトでない場合は、オプションのメッセージmsg
と共にテストは失敗します。refute_same(exp, act, msg = nil)
exp
とact
が同じオブジェクトでないことを確認します。もし同じオブジェクトである場合は、オプションのメッセージ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)
exp
とact
の差分を表示します。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スタイル
編集- 構文: 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
- 概念の直接性: TestUnitスタイルは比較的直接的でシンプルです。テストクラスとテストメソッドを作成し、アサーションメソッドを使用して期待値と実際の値を比較します。
- テストの構造: テストはクラスとメソッドの階層構造を持ちます。これは、テストの整理や分類に便利です。
Specスタイル
編集- 構文: Specスタイルでは、
describe
とit
を使用してテストのグループ化と記述を行います。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
- ドメイン特化言語(DSL): SpecスタイルはDSLの特徴を持ち、テストが自然言語に近い形で書かれることがあります。
must
,wont
,should
,expect
などのメソッドがアサーションを表します。 - 期待される動作の表現: 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)
:exp
とact
が等しいことをアサートrefute_equal(exp, act)
:exp
とact
が等しくないことをアサート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
オブジェクトをインスタンス化し、テストケースの後にデータベース接続を閉じています。setup
とteardown
を適切に使うことで、テストの再現性と信頼性が高まります。
テストの実行
編集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では、テストケースの前後で実行したい処理をbefore
、after
、around
ブロックで記述できます。
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ではlet
やlet!
を使ってインスタンス変数を初期化することができ、とても重要な機能です。
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
が初期化されるので、その後に続くすべてのテストケースで同じオブジェクトを使えます。
let
やlet!
を使うことで、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を使用すると、以下のようなことが可能です:
- 依存関係の置き換え: テスト対象のコードが他のクラスやモジュールと連携している場合、それらの依存関係をモックオブジェクトで置き換えることができます。これにより、テストの際に外部の状態に依存することなく、コードの特定の部分の振る舞いを確認できます。
- テストの制御: モックオブジェクトを使用することで、テスト中に特定のメソッドが呼び出されたかどうかや、どのような引数で呼び出されたかなどを制御できます。これにより、特定の条件下での振る舞いをシミュレートしてテストを行うことができます。
- テストの独立性: モックを使用することで、テスト対象のコードが他のコンポーネントに依存している場合でも、それらのコンポーネントの実際の実装を使用せずにテストを行うことができます。これにより、テストの独立性が高まり、テストスイート全体の信頼性が向上します。
総括すると、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
この例では、MyClass
のmy_method
がテスト対象のメソッドです。テスト中にこのメソッドの振る舞いを制御するために、MyClass.stub
を使用して特定の返り値を設定しています。その後、通常のメソッド呼び出しの振る舞いが正しいことを確認します。
これにより、テスト中に外部リソースやモジュールの振る舞いを制御し、テストの安定性や再現性を向上させることができます。