Hatena::Grouprubyist

Going My Ruby Way このページをアンテナに追加 RSSフィード

Ruby ロゴ (C) Ruby Association LLC

2011年07月24日(日)

minitest/unit についてのメモ

| 20:50 | minitest/unit についてのメモ - Going My Ruby Way を含むブックマーク はてなブックマーク - minitest/unit についてのメモ - Going My Ruby Way minitest/unit についてのメモ - Going My Ruby Way のブックマークコメント

テスティングフレームワークについてのメモです。

(2011/07/24 Updates by ユニットテスト - Going My Ruby Way - Rubyist)

[参考] minitest/unit ドキュメント

----

例として、前に私が書いた string.rb をテストします。

string.rb の内容(抜粋)

class String
  def remove(s)
    sub(s, '')
  end

  alias - remove

  def wrap(w)
    w1 = w.at(0) || ''
    w2 = w.at(1) || ''
    w2 = w1 if w2.empty?

    w1 + self + w2
  end

  alias ** wrap

  def at(n)
    self[n, 1]
  end
      :
   (以下、略)

テストスクリプトは以下のような感じになります。

test_string.rb

#!/usr/bin/env ruby
# -*- coding: UTF-8 -*-
require 'string'      # テスト対象を require
require 'test/unit'   # 'test/unit' を require

class TestString < Test::Unit::TestCase  # 基底クラスは Test::Unit::TestCase
  # 前処理メソッド setup
  def setup
    @str = "foobarbaz"
  end

  # 後処理メソッド teardown
  def teardown
    # 今回は何もしない
  end

  def test_wrap   # テストメソッドの名前は test_ で始める
    # 
    # どのようにあるべきか、を表明するアサーションメソッド。
    # assert_equal はその 1 つ。actual が expect と値が等しいことを
    # 表明する。値が等しい場合はテストは成功(success)、等しくない場合は
    # 不成功(failure)、テスト自体が失敗した場合は error、となる。
    assert_equal expected = "(foobarbaz)",
                 actual   = @str ** "()" 
  end

  def test_remove
    assert_equal expected = "fooz",
                 actual   = @str - "baz"
    # このアサーションは不成功(failure)になる。
    # この場合、テスト対象のバグでなくて、期待値(expected) の
    # 記述ミス。正しくは expected = "foobaz"。
  end

  def test_ouch
    ouch!   
    # (露骨な例ですが)これは文法違反(NoMethodError)なので
    # テスト自体の失敗(error)になる。
  end
end

# vim:set ts=2 sw=2 et fenc=UTF-8:

テストメソッドは

  setup -> test_wrap -> teardown
  setup -> test_remove -> teardown 

のように毎回前処理/後処理の setup/teardown に挟まれて呼び出されます。

では、実行します。するとレポートが表示されます。

$ ruby test_string.rb
Loaded suite test_string
Started
EF.
Finished in 0.001034 seconds.

  1) Error:
test_ouch(TestString):
NoMethodError: undefined method `ouch!' for #<TestString:0x00000000b5d178>
    test_string.rb:37:in `test_ouch'

  2) Failure:
test_remove(TestString) [test_string.rb:28]:
<"fooz"> expected but was
<"foobar">.

3 tests, 2 assertions, 1 failures, 1 errors, 0 skips

Test run options: --seed 47747

3 つのテストのうち、1件不成功(failure)、1件エラー(error) です。

error については(たいていの場合)テストスクリプトの修正で対処します。

  test_ouch のメソッド定義を削除。(いらないものなので)

failure については(たいていの場合)テスト対象をデバッグして対処します。

しかし、上の例のようにテスト仕様が間違っている場合もあります。

今回は、テストスクリプトの test_remove を以下のように修正します。

  def test_remove
    assert_equal expected = "foobaz",    # 期待値を正しいものに修正
                 actual   = @str - "bar"
  end

再度、実行します。

$ ruby test_string.rb 
Loaded suite test_string
Started
..
Finished in 0.000594 seconds.

2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

Test run options: --seed 13629

今度は 0 failures, 0 errors 。テスト成功です。

----

Ruby1.9 では minitest が使用できます。

require 'minitest/unit'
require 'minitest/autorun'

class TestFoo < MiniTest::Unit::TestCase
  def setup
     ....
  end

  def teardown
     ....
  end

  def test_....
     ...
  end

'test/unit'を require すると minitest の互換モードが走るらしいです。(test/unit と見た目変わらないようです)

------

その他のアサーション。いろいろありますが数例だけ挙げます。

   assert_raises(TypeError) do
     ....   # 例外を起こすはずのコード
   end

   assert_not_raised do
     ....   # 例外を起こさないはずのコード
   end

   assert_block do
     ....   # ブロックを評価して真ならテストパス。汎用。
   end

------

tips。assert_equal の 3番目の引数はメッセージ。

最初のテスト不成功になるスクリプトの test_remove を以下のように書くと、

  def test_remove
    assert_equal expected = "fooz",
                 actual   = @str - "bar",
                 message  = "文字列から文字列を削除するテスト"
  end

実行した時のレポートに不成功した時のメッセージが表示されます。

$ ruby test_string.rb
Loaded suite test_string
Started
F.
Finished in 0.000925 seconds.

  1) Failure:
test_remove(TestString) [test_string.rb:28]:
文字列から文字列を削除するテスト.
<"fooz"> expected but was
<"foobar">.

2 tests, 2 assertions, 1 failures, 0 errors, 0 skips

Test run options: --seed 52601

その他のアサーションメソッドにもメッセージが指定できます。詳しくは以下を参照。

余談。assert_equal の引数の指定に expected = とかを書いているのは分かりやすくするためです。もちろん無くていいです。

メッセージ指定の時に、カンマを忘れて下のように書くと

  def test_remove
    assert_equal expected = "fooz",
                 actual   = @str - "bar"    # <-- カンマ(,)を忘れる
                 message  = "文字列から文字列を削除するテスト"
  end

以下のように解釈されてしまいます。

  def test_remove
    assert_equal(expected = "fooz", actual = @str - "bar") 
    message  = "文字列から文字列を削除するテスト"
  end

よって message が assert_equal のオプションと見做されなくなってしまいます。

注意しましょう。(私、何回もやりました。。。)

----

tips(かな?)。こんな風にも書けます。

(t メソッドを作らず、proc (Ruby1.9 なら -> も可) でもいいです)

  def t(&block)
    Proc.new(&block)
  end

  def test_remove
    {
      t{ @str - ""    }      => t{ "foobarbaz"  },
      t{ @str - "foo" }      => t{ "barbaz"     },
      t{ @str - "bar" }      => t{ "foobaz"     },
      t{ @str - "baz" }      => t{ "foobar"     },
      t{ @str - "fo"  }      => t{ "obarbaz"    },
      t{ @str - "ba"  }      => t{ "foorbaz"    },

      t{ @str - "xyz" }      => t{ "foobarbaz"  },
      t{ @str - "fa"  }      => t{ "foobarbaz"  },

      t{ @str - /ba./ }      => t{ "foobaz"     },
      t{ @str - /ba.$/}      => t{ "foobar"     },

    }.each do |actual, expected|
      assert_equal expected[], actual[]
    end
  end

このテストの 10 個のアサーションは全部成功します。

----

tips。テストスクリプト実行時に --name オプションでテストするテストメソッドを個別に指定できます。以下は、--name で test_wrap を指定した例です。

$ ruby test_string.rb --name test_wrap
Loaded suite test_string
Started
.
Finished in 0.000580 seconds.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

Test run options: --seed 24092 --name "test_wrap"

------

tips。サブディレクトリ test の test_*.rb をテストする Rakefile の例です。

(『Ruby ベストプラクティス』にも載ってます)

#!/usr/bin/env ruby
# -*- coding: UTF-8 -*-
require 'rake/testtask'

task :default => [:test]

Rake::TestTask.new do |test|
  test.libs << "test"
  test.test_files = Dir["test/test_*.rb"]
  test.verbose = true
end

# vi:se ts=2 sw=2 et fenc=UTF-8:

rake の実行は、以下のようにします。

$ rake test
または
$ rake

-------

tips。モジュールのメソッドをテストするとき、以下のようにします。

  module Foo
    # テスト対象のモジュール
    def a
        :
  end

  class TestFooModule
    include Foo  # ダミーのクラスに include してテストする
  end

  class TestFoo < Test::Unit::TestCase
    def setup
      @c = TestFooModule.new   # インスタンスを作る
    end

Foo を include したクラスのインスタンスを直接作る例。

  module Foo
    # テスト対象のモジュール
    def a
        :
  end

  class TestFoo < Test::Unit::TestCase
    def setup
      @c = Class.new{ include Foo }.new   # インスタンスを直接作る
    end
        : 

-------

有用そうなライブラリ。『Ruby ベストプラクティス』では

  • モックには flexmock ライブラリ
  • 偽データ生成には faker ライブラリ

を勧めています。

-------

参考書籍

第1章で TDD / テスティングフレームワークについて書かれている。

test/unit の本。

Rubyを256倍使うための本 極道編

Rubyを256倍使うための本 極道編