ようへいの日々精進XP

よかろうもん

Minitest で始める Ruby のテスト

はじめに

この記事は, プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで の読書メモ的な何かです. この書籍はテストの自動化について章を割いているし, 各章でもテストを書いてからサンプルコードを実装していくという流れになっていて, 実際の開発の流れを体感出来るのでとても良い本だと思います. ということで, 今回は書籍で取り上げている Minitest を触りながら Ruby のテストの入り口を覗いてみたいと思います.

尚, 本記事では特に断りが無い限りで, 以下のバージョン Ruby を利用して検証を行っています.

$ ruby --version
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]

それでは, レッツテスト.

Minitest

Minitest とは, Ruby に始めから導入されているテストフレームワークで, 特にセットアップの必要なく利用できます.

require 'minitest/autorun'

class FooTest < Minitest::Test
  def test_foo
    assert_equal 'foo', 'foo'
  end
end

実行すると, 以下のように.

$ ruby foo_test.rb 
Run options: --seed 33722

# Running:

.

Finished in 0.001728s, 578.6997 runs/s, 578.6997 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

ちなみに, Ruby にテストフレームワークについて, 以下の記事がとても面白かったです.

Rspec くらいしか知らなかったけど, Rspec 以外にも色んなテストフレームワークがあったんだなと. Ruby に歴史あり, テストフレームワークに歴史あり.

レッツ Minitest

サンプルコード

プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで 第七章「クラスの作成を理解する」の「7.7.7 protected メソッド」に掲載されている体重比較クラス.

class User
  attr_reader :name
  def initialize(name, weight)
    @name = name
    @weight = weight
  end

  def heavier_than?(other_user)
    other_user.weight < @weight
  end

  protected
  def weight
    @weight
  end
end

以下のように, 利用します.

[2] pry(main)> foo = User.new('foo', 50)
=> #<User:0x000056280c7c0a58 @name="foo", @weight=50>
[3] pry(main)> bar = User.new('bar', 60)
=> #<User:0x000056280c781970 @name="bar", @weight=60>
[4] pry(main)> foo.heavier_than?(bar)
=> false
[5] pry(main)> bar.heavier_than?(foo)
=> true

protected メソッドは同じクラスやサブクラス内から, レシーバ付きで呼び出すことが出来るというのが private メソッドと異なる点. public と private と比べて利用頻度が少なそうなんだけど, protected メソッドの使いドコロが具体的なコードで説明されていて参考になりました.

テストコード

サンプルコードとテストコード

ところで, プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで では, 各章のサンプルコードには殆どと言っていいほどテストコードが付いているので, この体重比較クラスについて, 以下のようなテストコードを考えてみました.

テストコード (1)

まず, User#heavier_than? をテストするだけのコード.

require 'minitest/autorun'
require './weight'

class WeightTest < Minitest::Test

  def setup
    @foo = User.new('Foo', 50)
    @bar = User.new('Bar', 60)
  end

  def test_foo_heavier_than_bar?
    assert_equal @foo.heavier_than?(@bar), false
  end

  def test_bar_heavier_than_foo?
    assert_equal @bar.heavier_than?(@foo), true
  end
end

実行してみます.

$ bundle exec ruby weight1_test.rb 
Run options: --seed 41780

# Running:

..

Finished in 0.000681s, 2936.4863 runs/s, 2936.4863 assertions/s.

2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

いい感じ.

テストコード (2)

protected メソッドが外部から呼べないことを確認してみます.

require 'minitest/autorun'
require "minitest/reporters"

class WeightTest < Minitest::Test

  def setup
    @foo = User.new('Foo', 50)
    @bar = User.new('Bar', 60)
  end
... 略 ...

  def test_call_foo_weight
    assert_respond_to(@foo, :weight)
  end

  def test_call_bar_weight
    assert_respond_to(@bar, :weight)
  end
end

MiniTest::Assertions#assert_respond_to を使うことで, 指定したオブジェクトでメソッドが呼び出し可能かをテストすることが出来ます.

$ bundle exec ruby weight_test.rb 
Run options: --seed 721

# Running:

FF

Finished in 0.000811s, 2466.9188 runs/s, 2466.9188 assertions/s.

  1) Failure:
WeightTest#test_call_bar_weight [weight_test.rb:27]:
Expected #<User:0x000055794dafccd0 @name="Bar", @weight=60> (User) to respond to #weight.

  2) Failure:
WeightTest#test_call_foo_weight [weight_test.rb:23]:
Expected #<User:0x000055794daf2ca8 @name="Foo", @weight=50> (User) to respond to #weight.

2 runs, 2 assertions, 2 failures, 0 errors, 0 skips

見事にコケたので, 呼び出すことは出来ないことが確認出来ました.

テストコード (3)

protected メソッドである User#weight のテストも追加してみます.

require 'minitest/autorun'
require "minitest/reporters"

class WeightTest < Minitest::Test

  def setup
    @foo = User.new('Foo', 50)
    @bar = User.new('Bar', 60)
  end
... 略 ...
  def test_foo_weight
    assert_equal @foo.send(:weight), 50
  end

  def test_bar_weight
    assert_equal @bar.send(:weight), 60
  end
end

private メソッドや protected メソッドのテストは, 以下のように Object#send メソッドを利用して呼び出すようにしました.

$ bundle exec ruby weight2_test.rb 
Run options: --seed 53260

# Running:

....

Finished in 0.000997s, 4013.8320 runs/s, 4013.8320 assertions/s.

4 runs, 4 assertions, 0 failures, 0 errors, 0 skips

いい感じ.

テストコード (最終形態)

ちょっとテスト結果の出力が寂しいので minitest-reporters を利用して, テストの結果を少しだけ賑やかにしてみたいと思います.

github.com

minitest-reporters の詳細については割愛しますが, 以下のようなクラスを指定することで, 指定したフォーマットで出力させることが出来るようです.

Minitest::Reporters::DefaultReporter  # => Redgreen-capable version of standard Minitest reporter
Minitest::Reporters::SpecReporter     # => Turn-like output that reads like a spec
Minitest::Reporters::ProgressReporter # => Fuubar-like output with a progress bar
Minitest::Reporters::RubyMateReporter # => Simple reporter designed for RubyMate
Minitest::Reporters::RubyMineReporter # => Reporter designed for RubyMine IDE and TeamCity CI server
Minitest::Reporters::JUnitReporter    # => JUnit test reporter designed for JetBrains TeamCity
Minitest::Reporters::MeanTimeReporter # => Produces a report summary showing the slowest running tests
Minitest::Reporters::HtmlReporter     # => Generates an HTML report of the test results

今回は, Minitest::Reporters::SpecReporter を使って, 以下のように書きました.

require 'minitest/autorun'
require "minitest/reporters"
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]

require './weight'

class WeightTest < Minitest::Test

  def setup
    @foo = User.new('Foo', 50)
    @bar = User.new('Bar', 60)
  end

  def test_foo_heavier_than_bar
    assert_equal @foo.heavier_than?(@bar), false
  end

  def test_bar_heavier_than_foo
    assert_equal @bar.heavier_than?(@foo), true
  end

  def test_foo_weight
    assert_equal @foo.send(:weight), 50
  end

  def test_bar_weight
    assert_equal @bar.send(:weight), 60
  end
end

実行すると, 以下のように出力されました.

$ bundle exec ruby weight_test.rb 
Started with run options --seed 56419

WeightTest
  test_bar_heavier_than_foo                                       PASS (0.00s)

WeightTest
  test_foo_heavier_than_bar                                       PASS (0.00s)

WeightTest
  test_foo_weight                                                 PASS (0.00s)

WeightTest
  test_bar_weight                                                 PASS (0.00s)

Finished in 0.00083s
4 tests, 4 assertions, 0 failures, 0 errors, 0 skips

ちなみに, minitest-reporters を利用しない場合には...

$ bundle exec ruby weight_test.rb 
Run options: --seed 10588

# Running:

....

Finished in 0.001064s, 3758.5227 runs/s, 3758.5227 assertions/s.

4 runs, 4 assertions, 0 failures, 0 errors, 0 skips

ま, これでも悪くないですが.

以上

Minitest をちょっと触ってみました. プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法までのおかげでだいぶん Ruby のテストに対する心の壁が取り払われたような気がします.

有難う御座いました.