須藤です。

日本語でごめんなさい。

ruby-coreでの話なのですが、Test::Unitをminiunitで置き換えよう
としていたのを私だけが反対していたという件についてです。

In <E1KIEXZ-0005M6-Sc / x61.netlab.jp>
  "[ruby-core:17758] Re: Release Plan: Ruby 1.9.0-2" on Mon, 14 Jul 2008 12:11:43 +0900,
  Yukihiro Matsumoto <matz / ruby-lang.org> wrote:

> |I stop to object that Test::Unit is replaced with miniunit.
> |Because nobody objects it except me. It will mean that my
> |opinion doesn't make sense. If Matz says 'go', Test::Unit
> |will be replaced with miniunit.
> |
> |I'll write my opinions in this mail below. I'm happy if my
> |opinions are considered a bit in the future. It's OK for me
> |if they aren't considered.
> 
> If I understand you correctly, lack of full compatibility is
> the primary reason of your objection to minitest, right?

私がminiunitへの置き換えに反対していた1番の理由(*)は、
miniunit にはTest::Unit以上に拡張性がないことです。

(*) 1番の理由というよりは、妥協して最後に残った、少なくとも
    これだけはお願い、という理由

ただ、↑にも書いたとおり、今は反対していません。大きな理由は
以下です。

  * 彼らに私の言いたいことを伝えるのが難しいことがわかった
    (私の英語力やdiscussionの仕方では)

  * 彼らの方針は「標準添付じゃなくて別にやればいいじゃないか」
    のようだ

反対しているのは私だけだったようなので、miniunitで置き換える
のにmore discussionは必要ないんじゃないかと思っています。



もう必要ないと思いますが、せっかくなのでメンテナと私の意見を
以下に書いておきます。
(主に私の意見が)長いです。ごめんなさい。

何人か反応していた人の意見では、メンテナの意見でいいんじゃな
い?がほとんどでした。


== 前提

  * 現Test::UnitのメンテナRyan DavisはTest::Unitに問題がある
    と思っていた

  * 私も問題があると思っていた


== メンテナの意見

=== Test::Unitの問題点

複雑なのでメンテナンス性が悪く(外向きのAPIじゃなく内部のAPI
や構造などを)使ったり拡張したりするのが大変

=== 解決法

Test::Unitをminiunitで置き換える

miniunitは以下の特徴を持ったテスティングフレームワーク

  * シンプル
  * 小さい
  * きれい
  * Test::Unitと互換のAPIも提供している

=== 利点

  * メンテナンスしやすい
  * 速い
  * Test::Unitより機能が豊富
    * アサーションが多い
      * 多分、以下とrefute_*のこと。refute_*はassert_not_*と同じ
      * assert_empty
      * assert_in_epsilon
      * assert_includes
    * 小さいBDD用のインターフェイスがついている
    * 小さいモックの実装がついている

=== 欠点

なし。今のTest::Unitで書かれたテストはそのまま動くし。



== 私の意見

私は以下の理由からテスト環境はできるだけ機能豊富であって欲し
いと思っています。これはテストを書く1ユーザとしての視点から
です。

  * テストを書くのが面倒くさくならないようにしたい
    * テストを簡単に書きたい
    * テストを簡潔に書きたい
  * テストが失敗したときにできるだけ早く問題を特定・解決した
    い(少なくとも特定したい)
    * 問題箇所が見つけやすいようにテスト結果を表示して欲しい

そして、標準のRubyパッケージがテストを書く人にやさしいパッケー
ジになって欲しいと思っています。もし、それが無理でもせめて外
部パッケージで拡張しやすいようになって欲しいと思っています。

=== Test::Unitの問題点

  * 拡張しづらい
  * 機能豊富ではない

==== 拡張しづらい

例えば、MochaやActiveSupportではTest::Unit::TestCase#runを以
下のようにまるごと置き換えています。

  alias_method :run_before_mocha_test_case_adapter, :run
  def run(result)
    assertion_counter = AssertionCounter.new(result)
    yield(Test::Unit::TestCase::STARTED, name)
    @_result = result
    begin
      begin
        setup
        __send__(@method_name)
        mocha_verify(assertion_counter)
      rescue Mocha::ExpectationError => e
        add_failure(e.message, e.backtrace)
      rescue Test::Unit::AssertionFailedError => e
        add_failure(e.message, e.backtrace)
      rescue Exception
        raise if Test::Unit::TestCase::PASSTHROUGH_EXCEPTIONS.include? $!.class
        add_error($!)
      ensure
        begin
          teardown
        rescue Test::Unit::AssertionFailedError => e
          add_failure(e.message, e.backtrace)
        rescue Exception
          raise if Test::Unit::TestCase::PASSTHROUGH_EXCEPTIONS.include? $!.class
          add_error($!)
        end
      end
    ensure
      mocha_teardown
    end
    result.add_run
    yield(Test::Unit::TestCase::FINISHED, name)
  end

この例では、入れたいのはmocha_verityとmocha_teardownだけです。
そのためにメソッド全体をコピペして利用しなければいけない構造
になっています。このため、Test::Unitを拡張したいライブラリの
メンテナンス性が悪くなります。(Test::Unitの内部のAPIが変わっ
たら動かなくなるかもしれない)

==== 機能豊富ではない

Test::Unitはここ4-5年くらいテスティングフレームワークとしての
機能のうち、テストをもっと書きやすくするための機能(例えばア
サーション)はほとんど変更されていません。ほそぼそと行われる
変更のほとんどはテスト起動まわりとRuby本体の仕様変更への追従
です。(その多くはTest::Unitメンテナ以外による)

しかし、その4-5年の間もテスト手法は改良されています。例えば、
モックオブジェクトを使ったテスト手法を使えば、もっと便利にテ
ストが書けるでしょう。expectedとactualのdiffを表示して問題点
を発見しやすくする手法を使えば、もっと楽にテストをメンテナン
スできるでしょう。


もっとテストを便利に書きたいプログラマの1人としては、現在の
Test::Unitは少し伝統的過ぎる感があります。

=== miniunitの問題点

↑のTest::Unitの問題点と同じ。miniunitに変えても問題点が改善
されないどころかもっと悪くなると思ったので反対していました。

==== 拡張しづらい

メンテナが言うには、Rubyで書かれているんだからminiunitも
test/unitと同じくらい拡張性がある、らしいです。ただ、私は
test/unitは拡張性が低いと思っているので、miniunitにしても嬉
しくないと思っています。

miniunitの場合でもTest::Unitのように#runをコピペして上書きっ
ぽいことをしないといけません。ActiveSupportではそのようにして
います。関係ないですが、ActiveSupportのやり方だとteardownが2
回呼ばれそうな気がします。


また、miniunitはBDD用の構文もサポートしているのですが、そのた
めのmust_*メソッド(RSpecと違ってshouldではない)は
Module#infect_with_assertionsでassert_*/refute_*から動的に生
成しています。ただ、動的といってもrequire 'mini/spec'した時
点で生成するので独自assert_*を作った場合は自分でmust_*を定義
しないといけません。ただ、Module#infect_with_assertionsはそ
んなに使いやすそうではありません。(拡張しづらそう)

miniunitはsimpleなのをウリにしていますが、このようにevalして
動的にメソッドを定義したりするはsimple?とか思ったりします。


==== 機能豊富ではない

miniunitにはモックオブジェクトのサポートが入っていますが、ソー
スコードが小さいだけがウリのようなおもちゃのようなもので、こ
れで便利にテストが書けるとは思えません。

例えば、可変長引数に対応していないので、こういうことができま
せん。

  mock = Mini::Mock.new
  mock.expect(:meaning_of_life, 42)
  mock.expect(:meaning_of_life, 42 ** 2, [:arg1, :arg2])

  assert_equal(42, @mock.meaning_of_life)
  assert_equal(42 ** 2, @mock.meaning_of_life(:arg1, :arg2))
  mock.verify

mock.expect(:meaning_of_life, 42 ** 2, :arg1, :arg2)と書けな
いことやmock.verifyを明示的に呼び出さなければいけないのもど
うかと思います。

また、miniunitはエラーメッセージがまるで親切ではありません。
例えば、このような失敗するテストがあるとします。

  mock = Mini::Mock.new
  mock.expect(:meaning_of_life, 42)
  assert_equal(42, mock.meaning_of_life(:arg1))

このときのエラーメッセージはこうなります。

  ...
    1) Error:
  test_should_create_stub_method(TestMiniMock):
  ArgumentError: ArgumentError
      /var/lib/gems/1.8/gems/miniunit-1.2.1/lib/mini/mock.rb:15:in `meaning_of_life'
      x.rb:14:in `test_should_create_stub_method'
      ...

メッセージなしのArgumentErrorです。何が悪いのかがわかりづら
いです。

mock.verifyで失敗したときも不親切です。

  mock = Mini::Mock.new
  mock.expect(:meaning_of_life, 42)
  mock.expect(:meaning_of_life, 42 ** 2, [:arg1, :arg2])
  mock.verify

こうなります。

  ...
    1) Error:
  test_should_create_stub_method(TestMiniMock):
  MockExpectationError: expected meaning_of_life, {:retval=>1764, :args=>[:arg1, :arg2]}
      /var/lib/gems/1.8/gems/miniunit-1.2.1/lib/mini/mock.rb:26:in `verify'
      /var/lib/gems/1.8/gems/miniunit-1.2.1/lib/mini/mock.rb:23:in `each_key'
      /var/lib/gems/1.8/gems/miniunit-1.2.1/lib/mini/mock.rb:23:in `verify'
      x.rb:15:in `test_should_create_stub_method'
      ...

呼ばれていないexpectのうち1つだけしか表示されません。また、
失敗ではなくエラーになっているのもどうかと思います。

他にも、expectしていない引数で呼ばれたときをチェックできない
など機能不足な感があります。


また、エラーメッセージが不親切なのはassert_equalもそうです。

    assert_equal("IIIIIIIIIIIIIIIIIIIIlIIIIIIIIIIIII",
                 "IIIIIIlIIIIIIIIIIIIIIIIIIIIIIIIIII")

この場合、以下のように表示されます。

    1) Failure:
  test_should_create_stub_method(TestMiniMock) [x.rb:13]:
  Expected "IIIIIIIIIIIIIIIIIIIIlIIIIIIIIIIIII", not "IIIIIIlIIIIIIIIIIIIIIIIIIIIIIIIIII".

ちなみにTest::Unitはこうです。

    1) Failure:
  test_should_create_stub_method(TestMiniMock) [x.rb:5]:
  <"IIIIIIIIIIIIIIIIIIIIlIIIIIIIIIIIII"> expected but was
  <"IIIIIIlIIIIIIIIIIIIIIIIIIIIIIIIIII">.

私はminiunitよりもTest::Unitの方がテストを書く人、テストをメ
ンテナンスする人のこと考えていると思えます。


また、miniの名前の通り、メンテナは現在のminiunitの実装で十分
な機能があると判断しているらしく、これ以上機能が拡張されるこ
とはほとんどないと考えられます。何もしないのだから、それはた
しかにメンテナンスコストは下がるというのはその通りだと思いま
す。

=== 解決法

ruby-coreの方には書きましたが、特に反応もなかったので省略。


今となっては、自分で使うためのテスティングフレームワークは自
分で作ろうと思っています。