Minitest
Minitest
概要
MinitestはRuby用の完全なテストスイートです。TDD、BDD、モック、ベンチマークをサポートする軽量で高速なテストフレームワークとして設計されています。Ruby 1.9以降の標準ライブラリとして採用され、Test::Unitの後継として位置づけられています。シンプルで直感的なAPIと豊富な機能により、現代のRuby開発における事実上のテスト標準となっています。
詳細
主な特徴
- 完全なテストスイート: TDD、BDD、モック、ベンチマークを統合提供
- 軽量設計: 最小限の依存関係で高速動作
- 柔軟なスタイル: Test::Unitスタイルとspec(RSpec風)スタイルの両方をサポート
- スタブ機能: 組み込みのスタブ・モック機能
- 並列実行: マルチプロセス・マルチスレッドでの並列テスト実行
- ベンチマーク: 内蔵されたパフォーマンス測定機能
- カスタマイズ可能: プラグインとリポーターによる拡張性
アーキテクチャ
Minitestは以下の核心コンポーネントで構成されています:
- Test: 基本テストクラス(Test::Unit互換)
- Spec: spec記法によるBDDスタイルテスト
- Mock: モックオブジェクトとスタブ機能
- Benchmark: パフォーマンス測定とベンチマーク
- Reporter: テスト結果の表示とフォーマット
- Parallel: 並列テスト実行エンジン
メリット・デメリット
メリット
- 標準統合: Ruby標準ライブラリとして安定した提供
- 高性能: 軽量設計による高速なテスト実行
- 多様なスタイル: TDDとBDDの両方に対応
- 組み込み機能: モック、スタブ、ベンチマークが標準装備
- 並列実行: 大規模テストスイートの高速実行
- 豊富なアサーション: 多様なテストシナリオに対応
デメリット
- シンプル設計: 高度なBDD機能はRSpecに劣る
- エコシステム: RSpecほど豊富なプラグインエコシステムはない
- 学習コスト: 複数のスタイルによる初学者の混乱
- 高度なモック: より複雑なモック機能は外部ライブラリが必要
参考ページ
書き方の例
基本的なTest::Unitスタイル
require 'minitest/autorun'
class CalculatorTest < Minitest::Test
def setup
@calculator = Calculator.new
end
def test_addition
assert_equal 4, @calculator.add(2, 2)
assert_equal 0, @calculator.add(-1, 1)
assert_equal -3, @calculator.add(-1, -2)
end
def test_subtraction
assert_equal 0, @calculator.subtract(2, 2)
assert_equal 5, @calculator.subtract(8, 3)
assert_equal -1, @calculator.subtract(2, 3)
end
def test_division_by_zero
assert_raises(ZeroDivisionError) do
@calculator.divide(5, 0)
end
end
def test_with_detailed_message
result = @calculator.multiply(3, 4)
assert_equal 12, result, "3 * 4 should equal 12"
end
end
Specスタイル(BDD)
require 'minitest/autorun'
describe Calculator do
before do
@calculator = Calculator.new
end
describe "addition" do
it "adds positive numbers" do
_(@calculator.add(2, 3)).must_equal 5
end
it "adds negative numbers" do
_(@calculator.add(-2, -3)).must_equal -5
end
it "handles zero" do
_(@calculator.add(0, 5)).must_equal 5
_(@calculator.add(5, 0)).must_equal 5
end
end
describe "division" do
it "divides numbers correctly" do
_(@calculator.divide(10, 2)).must_equal 5
end
it "raises error for division by zero" do
-> { @calculator.divide(5, 0) }.must_raise ZeroDivisionError
end
end
describe "edge cases" do
it "handles floating point numbers" do
result = @calculator.add(0.1, 0.2)
_(result).must_be_within_delta 0.3, 0.001
end
end
end
豊富なアサーション
require 'minitest/autorun'
class AssertionTest < Minitest::Test
def test_equality_assertions
# 基本的な等価性
assert_equal "hello", "hello"
refute_equal "hello", "world"
# オブジェクトの同一性
obj = Object.new
assert_same obj, obj
refute_same Object.new, Object.new
end
def test_boolean_assertions
assert true
refute false
# nil チェック
assert_nil nil
refute_nil "not nil"
end
def test_collection_assertions
array = [1, 2, 3, 4, 5]
assert_includes array, 3
refute_includes array, 6
assert_empty []
refute_empty array
end
def test_type_assertions
assert_instance_of String, "hello"
assert_kind_of Numeric, 42
assert_respond_to [], :push
end
def test_pattern_assertions
assert_match /hello/, "hello world"
refute_match /goodbye/, "hello world"
end
def test_exception_assertions
assert_raises(ArgumentError) do
raise ArgumentError, "test error"
end
exception = assert_raises(RuntimeError) do
raise "custom error"
end
assert_equal "custom error", exception.message
end
def test_numeric_assertions
assert_in_delta 3.14159, Math::PI, 0.001
assert_in_epsilon 1000, 1010, 0.1 # 10%の誤差を許容
assert_operator 5, :>, 3
assert_operator "abc", :<, "def"
end
end
スタブとモック
require 'minitest/autorun'
class StubMockTest < Minitest::Test
def test_stubbing_methods
# メソッドのスタブ化
Time.stub :now, Time.at(0) do
assert_equal Time.at(0), Time.now
end
# 複数の値を順次返すスタブ
sequence = [1, 2, 3].each
Number.stub :random, -> { sequence.next } do
assert_equal 1, Number.random
assert_equal 2, Number.random
assert_equal 3, Number.random
end
end
def test_mock_objects
# モックオブジェクトの作成
mock_logger = Minitest::Mock.new
# 期待する呼び出しの設定
mock_logger.expect :info, true, ["Processing started"]
mock_logger.expect :info, true, ["Processing completed"]
# モックを使用するコードのテスト
processor = DataProcessor.new(mock_logger)
processor.process_data("test data")
# 期待した呼び出しがすべて行われたかを検証
mock_logger.verify
end
def test_partial_mocking
user = User.new("John")
# 既存オブジェクトの一部メソッドをスタブ
user.stub :save, true do
assert user.update_profile("New Name")
end
end
def test_stub_with_different_arguments
calculator = Calculator.new
# 引数に応じて異なる戻り値を設定
calculator.stub :add, 10 do
calculator.stub :add, 20 do
assert_equal 10, calculator.add(2, 3)
end
end
end
end
データ駆動テスト
require 'minitest/autorun'
class DataDrivenTest < Minitest::Test
# テストデータの定義
TEST_CASES = [
{ input: [2, 3], expected: 5, operation: :add },
{ input: [10, 4], expected: 6, operation: :subtract },
{ input: [3, 4], expected: 12, operation: :multiply },
{ input: [15, 3], expected: 5, operation: :divide }
]
def test_calculator_operations
calculator = Calculator.new
TEST_CASES.each do |test_case|
result = calculator.send(test_case[:operation], *test_case[:input])
assert_equal test_case[:expected], result,
"Failed #{test_case[:operation]} with #{test_case[:input]}"
end
end
# 動的にテストメソッドを生成
TEST_CASES.each_with_index do |test_case, index|
define_method "test_#{test_case[:operation]}_case_#{index}" do
calculator = Calculator.new
result = calculator.send(test_case[:operation], *test_case[:input])
assert_equal test_case[:expected], result
end
end
end
並列テスト実行
require 'minitest/autorun'
# 並列実行の有効化
parallelize_me!
class ParallelTest < Minitest::Test
def test_independent_operation_1
# 独立したテスト処理
sleep 0.1
assert_equal 4, 2 + 2
end
def test_independent_operation_2
# 独立したテスト処理
sleep 0.1
assert_equal 6, 2 * 3
end
def test_independent_operation_3
# 独立したテスト処理
sleep 0.1
assert_equal 1, 3 - 2
end
end
# 並列実行を避けたいテストクラス
class SerialTest < Minitest::Test
# このクラスは並列実行しない
def test_shared_resource_operation
# 共有リソースを使用するテスト
GlobalCounter.increment
assert_equal 1, GlobalCounter.value
end
end
ベンチマークテスト
require 'minitest/autorun'
require 'minitest/benchmark'
class BenchmarkTest < Minitest::Benchmark
# ベンチマーク対象のサイズ範囲を指定
def self.bench_range
[100, 1_000, 10_000, 100_000]
end
def bench_array_creation
assert_performance_linear 0.9999 do |n|
Array.new(n) { |i| i }
end
end
def bench_hash_lookup
data = Hash[(1..input_size).map { |i| [i, i * 2] }]
assert_performance_constant 0.99 do |n|
n.times { data[rand(input_size)] }
end
end
def bench_string_concatenation
# 線形性能を期待
assert_performance_linear 0.9 do |n|
str = ""
n.times { |i| str += i.to_s }
end
end
def bench_custom_algorithm
# カスタムアルゴリズムのベンチマーク
assert_performance_logarithmic 0.9 do |n|
binary_search(generate_sorted_array(n), n / 2)
end
end
private
def input_size
100_000
end
def binary_search(array, target)
# バイナリサーチの実装
low, high = 0, array.length - 1
while low <= high
mid = (low + high) / 2
case array[mid] <=> target
when -1 then low = mid + 1
when 1 then high = mid - 1
else return mid
end
end
-1
end
def generate_sorted_array(size)
(1..size).to_a
end
end
カスタムアサーションとヘルパー
require 'minitest/autorun'
module CustomAssertions
def assert_valid_email(email, message = nil)
message ||= "Expected #{email} to be a valid email"
email_regex = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
assert email_regex.match?(email), message
end
def assert_json_response(response, expected_status = 200)
assert_equal expected_status, response.status
assert_match /application\/json/, response.content_type
JSON.parse(response.body)
rescue JSON::ParserError
flunk "Response body is not valid JSON: #{response.body}"
end
def assert_difference(expression, difference = 1, message = nil, &block)
before = eval(expression)
yield
after = eval(expression)
actual_difference = after - before
assert_equal difference, actual_difference, message
end
end
class CustomTest < Minitest::Test
include CustomAssertions
def test_email_validation
assert_valid_email "[email protected]"
assert_raises(Minitest::Assertion) do
assert_valid_email "invalid-email"
end
end
def test_json_api_response
response = simulate_api_call
data = assert_json_response(response, 200)
assert_includes data, 'user_id'
assert_equal 'John', data['name']
end
def test_counter_increment
counter = Counter.new
assert_difference('counter.value', 2) do
counter.increment
counter.increment
end
end
private
def simulate_api_call
OpenStruct.new(
status: 200,
content_type: 'application/json',
body: '{"user_id": 123, "name": "John"}'
)
end
end
高度なテスト設定
require 'minitest/autorun'
# カスタムリポーターの作成
class CustomReporter < Minitest::AbstractReporter
def initialize(io = $stdout, options = {})
super
@io = io
end
def start
@io.puts "🚀 Starting test suite..."
end
def record(result)
case result.result_code
when '.'
@io.print "✅"
when 'F'
@io.print "❌"
when 'E'
@io.print "💥"
when 'S'
@io.print "⏭️"
end
end
def report
@io.puts "\n🏁 Test suite completed!"
super
end
end
# テスト実行前の設定
Minitest.reporter = CustomReporter.new
class AdvancedConfigTest < Minitest::Test
# テストの条件分岐
def test_environment_specific
skip "This test only runs in production" unless ENV['RAILS_ENV'] == 'production'
# 本番環境固有のテスト
assert production_feature_enabled?
end
def test_with_timeout
# タイムアウト付きテスト(外部ライブラリ使用)
Timeout::timeout(5) do
slow_operation
end
rescue Timeout::Error
flunk "Operation took too long"
end
def test_flaky_test_with_retry
retries = 0
begin
# 不安定なテスト処理
flaky_operation
rescue StandardError => e
retries += 1
retry if retries < 3
raise e
end
end
private
def production_feature_enabled?
# 本番環境のフィーチャーフラグチェック
true
end
def slow_operation
sleep 1
true
end
def flaky_operation
# 30%の確率で失敗するテスト
raise "Flaky failure" if rand < 0.3
true
end
end