Test::Unitでテストを書く


Rubyだと`MiniTest(Test::Unit)`が標準に入ってるけど機能が足りない感があり，`RSpec`もどうしたもんかなぁと思っていたんですが，gemでリリースされている`Test::Unit`が個人的には必要な機能が揃っていたので，今後はこれを中心にテストを書きたい．
ただ，`Test::Unit`のサイトのドキュメントはかなり簡素で毎回ソース読むはめになっているので，備忘録的に使い方をメモしておく．対象は最新リリースのv3.0.2．

- http://test-unit.github.io/
- https://github.com/test-unit/test-unit

# インストール

gemで入れるだけ

    $ gem install test-unit


# テストの書き方

## 基本

今までの`Test::Unit`と変わらないので，classで書く．ただ，昔の`Test::Unit`とは違い，`TestCase`毎に呼ばれる`startup`や`shutdown`などが増えている．

```rb
require 'test/unit'

class TestSample < Test::Unit::TestCase
  class << self
    # テスト群の実行前に呼ばれる．変な初期化トリックがいらなくなる
    def startup
      p :_startup
    end

    # テスト群の実行後に呼ばれる
    def shutdown
      p :_shutdown
    end
  end

  # 毎回テスト実行前に呼ばれる
  def setup
    p :setup
  end

  # テストがpassedになっている場合に，テスト実行後に呼ばれる．テスト後の状態確認とかに使える
  def cleanup
    p :cleanup
  end

  # 毎回テスト実行後に呼ばれる
  def teardown
    p :treadown
  end

  def test_foo
    p 'test_foo'
    assert_true(1 == 1)
  end

  def test_bar
    p 'test_bar'
    assert_equal(1, 1)
  end
end
```

これを実行すると以下のような結果になる．デフォルトだとアルファベット順にテストが実行される．

```text
Loaded suite test_sample
Started
:_startup
:setup
"test_bar"
:cleanup
:treadown
.:setup
"test_foo"
:cleanup
:treadown
.:_shutdown


Finished in 0.001366 seconds.

2 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

1464.13 tests/s, 1464.13 assertions/s
```

`test_bar`を失敗させてみると，失敗したテストでは`cleanup`が呼ばれてないことが分かる(いらない出力は削ってある)．

```text
Started
"test_bar"
F
===============================================================================
Failure:
test_bar(TestSample)
test_sample.rb:29:in `test_bar'
     26:
     27:   def test_bar
     28:     p 'test_bar'
  => 29:     assert_equal(1, 0)
     30:   end
     31: end
<1> expected but was
<0>

diff:
? 1
? 0
===============================================================================
"test_foo"
:cleanup
```

### アサーション

https://test-unit.github.io/test-unit/ja/Test/Unit/Assertions.html

のページの"Instance Method Summary"にあるassertで始まるメソッド群が該当．各メソッドに飛べば実際のコード例が出てくるので，参照しつつ適宜使い分ける．

## 便利な機能

### sub_test_case

RSpecだと`describe`とかでネスト出来るが，それをTest::Unitで出来る．実際はTest::Unitでも継承すれば出来るけど，こっちの方が簡潔で好き.

```rb
class TestSample < Test::Unit::TestCase
  sub_test_case "Foo context" do
    # tests
  end

  sub_test_case "Bar context" do
    # tests
  end
end
```

### test

個人的にRSpecで良かったのは，`it`とかで文字列ベースでテストが書けた所．メソッド名ベースだと使えない文字とかもあり，少し表現がしにくかった．けど，`test`を使えば解決！

```rb
class TestSample < Test::Unit::TestCase
  test "We can write good information" do
    # assert_nil(nil)
  end
end
```

Test::Unitのgemにはdescriptionという機能があって，それとメソッド定義のシンタックスシュガーらしい．以下のコードは上とほぼ同じ．

```rb
class TestSample < Test::Unit::TestCase
  description "We can write good information"
  def test_foo_bar
    # assert_nil(nil)
  end
end
```

### setup/teardownブロック

`sub_test_case`を使っていると，RSpecの`before`のように各ケース毎に追加でセットアップ処理を書きたくなる．TestCaseには`setup`メソッドがあるけど，これはオーバーライドしてしまうので使えない．このような場合には`setup`ブロックを使う．`setup`メソッドとは違い，`before`のように何個でも書ける．

```rb	
class TestSample < Test::Unit::TestCase
  setup do
    puts "parent"
  end

  sub_test_case "Sub!" do
    setup do
      puts "sub1"
    end

    setup do
      puts "sub2"
    end

    def test_sub
      assert_true(true)
    end
  end
end
```

上の例を走らせると以下のようになる．もちろん，`teardown`も同様に出来る．

```text
Loaded suite test_nest
Started
parent
sub1
sub2
.

Finished in 0.000896 seconds.
```

注意点として，`setup`メソッドと`setup`ブロックでは[優先順位が決まっている](https://github.com/test-unit/test-unit/blob/8892cbcda0de72722ecac306e3ebc89003cbd989/lib/test/unit/testcase.rb#L462)．気になるなら，`setup`ブロックで統一した方が良いかもしれない．

### test_order

テストの実行順序を指定出来る．どうしても定義順に実行したいのであれば，`:defined`を指定すれば上から順に実行される．

```rb
class TestSample < Test::Unit::TestCase
  self.test_order = :defined
end
```

### データ駆動テスト

テストとデータを分けて書ける機能です．成功するテストや失敗するテストをまとめたりするのに便利．テストの中で`each`回すのはイケてないし，かといってコピペで重複したテスト書くのもつらい．この機能使えば，ちゃんとデータセット毎にテストがわかれます．

```rb
class TestSample < Test::Unit::TestCase
  # 'test1'がラベルで，[1, 1]がtest_equalの引数に渡される
  data(
    'test1' => [1, 1],
    'test2' => [2, 2])
  def test_equal(data)
    expected, actual = data
    assert_equal(expected, actual)
  end

  data(
    'test1' => [1, 2],
    'test2' => [2, 3])
  def test_not_equal(data)
    expected, actual = data
    assert_not_equal(expected, actual)
  end
end
```

コメントで教えてもらいました．詳細は[Ruby用単体テストフレームワークtest-unitでのデータ駆動テストの紹介](http://www.clear-code.com/blog/2013/1/23.html)を参照．

# その他

## assertの作り方

`Test::Unit::Assertions`以下に定義して，そのファイルを`require`で読み込むだけ．`assert_block`とか`build_message`とかのヘルパーが利用可能．`assert_block`の中に条件を書く．

```rb
require 'test/unit/assertions'

module Test::Unit::Assertions
  def assert_oreore(expected, actual)
    assert_block("failure message") do
      expected == (actual + 'oreore')
    end
  end
end
```

## Rakeのタスク定義

コピペで使い回す

```rb
desc 'Run test_unit based test'
Rake::TestTask.new do |t|
  # To run test for only one file (or file path pattern)
  #  $ bundle exec rake test TEST=test/test_specified_path.rb
  t.libs << "test"
  t.test_files = Dir["test/**/test_*.rb"]
  t.verbose = true
end
```

# まとめ

RSpecほど機能がリッチじゃないので，足りないなら自分で書く必要があるけど，Fluentdレベルだとそんなに困ってない． ~~個人的には，Parameterized Testが入ってくれれば嬉しいという所かな．~~

何か他にも良い機能があったら追記するかもしれないし，しないかもしれない．