Edited at

Test::Unitでテストを書く

More than 3 years have passed since last update.

RubyだとMiniTest(Test::Unit)が標準に入ってるけど機能が足りない感があり,RSpecもどうしたもんかなぁと思っていたんですが,gemでリリースされているTest::Unitが個人的には必要な機能が揃っていたので,今後はこれを中心にテストを書きたい.

ただ,Test::Unitのサイトのドキュメントはかなり簡素で毎回ソース読むはめになっているので,備忘録的に使い方をメモしておく.対象は最新リリースのv3.0.2.


インストール

gemで入れるだけ

$ gem install test-unit


テストの書き方


基本

今までのTest::Unitと変わらないので,classで書く.ただ,昔のTest::Unitとは違い,TestCase毎に呼ばれるstartupshutdownなどが増えている.

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

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

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が呼ばれてないことが分かる(いらない出力は削ってある).

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でも継承すれば出来るけど,こっちの方が簡潔で好き.

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を使えば解決!

class TestSample < Test::Unit::TestCase

test "We can write good information" do
# assert_nil(nil)
end
end

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

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のように何個でも書ける.

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も同様に出来る.

Loaded suite test_nest

Started
parent
sub1
sub2
.

Finished in 0.000896 seconds.

注意点として,setupメソッドとsetupブロックでは優先順位が決まっている.気になるなら,setupブロックで統一した方が良いかもしれない.


test_order

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

class TestSample < Test::Unit::TestCase

self.test_order = :defined
end


データ駆動テスト

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

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でのデータ駆動テストの紹介を参照.


その他


assertの作り方

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

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のタスク定義

コピペで使い回す

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が入ってくれれば嬉しいという所かな.

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