Rubyのbundled gemのtest-unitをメンテナンスしている須藤です。
歴史
test-unitはxUnitスタイルのテスティングフレームワークです。Rubyのテスティングフレームワークの歴史(2014年版)にまとめてある通り、Ruby本体に標準添付されています。
Rubyに標準添付されているライブラリーには実は次の3種類あります。
-
ただの標準添付ライブラリー(例:
URI
)require
するだけで使えるライブラリー
-
default gem(例:csv)
-
require
するだけで使えるライブラリー -
RubyGemsで更新できる
-
Gemfile
でgemを指定しなくても使える
-
-
bundled gem(例:test-unit)
-
require
するだけで使えるライブラリー -
RubyGemsで更新できる
-
どれも標準添付ライブラリーなのでrequire
するだけで使えます。違いはRubyGems・Bundlerとの関係です。
ただの標準添付ライブラリーはRubyGemsでアップグレードすることはできませんし、Bundlerで特定のバージョンを指定することもできません。使っているRubyに含まれているものを使うだけです。
default gemはRubyGemsでアップグレードすることもできますし、Bundlerで特定のバージョンを指定することもできます。Bundlerを使っていてgem名を指定しなかった場合は使っているRubyに含まれているものを使います。
bundled gemはRubyGemsでアップグレードすることもできますし、Bundlerで特定のバージョンを指定することもできます。Bundlerを使っていてgem名を指定しなかった場合は使えません。Bundlerを使っていなければrequire
するだけで使えます。
Ruby 2.6.0でより高速になったcsvはRuby 2.6.0からdefault gemになっています。
test-unitはRuby 2.2.0で再度標準添付されるようになってからbundled gemになっています。
そんなtest-unitのデータ駆動テスト機能をさらに便利にしたものがRuby 2.6.0に入っています。
データ駆動テスト
データ駆動テストとは同じテスト内容をいろいろなデータで実行するテスト方法です。パラメーター化テストと呼ばれることもあります。いろいろな入力に対するテストを簡潔に書きたいときに便利です。
test-unitでは結構前からデータ駆動テストをサポートしています。
たとえば、正の数同士の足し算と負の数同士の足し算をテストすることを考えます。データ駆動テスト機能を使わない場合は次のようにそれぞれのケースについてテストを作ります。
require "test-unit"
class TestAdd < Test::Unit::TestCase
def test_positive_positive
assert_equal(3, my_add(1, 2))
end
def test_negative_negative
assert_equal(-3, my_add(-1, -2))
end
end
データ駆動テスト機能を使う場合はテストは1つで、テストに使うデータを複数書きます。
require "test-unit"
class TestAdd < Test::Unit::TestCase
data("positive + positive", [3, 1, 2])
data("negative + negative", [-3, -1, -2])
def test_add(data)
expected, augend, addend = data
assert_equal(expected, my_add(augend, addend))
end
end
データが増えてくるほど、データ駆動テスト機能を使った方がテストを書きやすくなります。データを追加するだけで済むからです。ただ、読みやすさは従来のテストの方が上です。テストに使うデータがベタ書きされているからです。
test-unitのデータ駆動テスト機能をもっと知りたくなった人はRuby用単体テストフレームワークtest-unitでのデータ駆動テストの紹介を参照してください。
データ表生成機能
Ruby 2.6.0に入っているtest-unitではデータ駆動テストがさらに便利になっています。
まだなんと呼ぶのがよいか決めかねているのですが、今のところデータ表(data matrix)と呼んでいるものを生成する機能が入っています。
データ表というのは各テストで使うデータをまとめたものです。前述のテストの場合は次のようになります。data
を使う毎に1行増えます。
ラベル | expected |
augend |
addend |
---|---|---|---|
"positive + positive" |
3 |
1 |
2 |
"negative + negative" |
-3 |
-1 |
-2 |
このデータ表をいい感じに生成する機能が入っています。
前述のテストで正の数と負の数を足す場合もテストしたくなったとします。その場合、従来のデータ駆動テスト機能の書き方では次のように書きます。data
を2つ増やしています。
require "test-unit"
class TestAdd < Test::Unit::TestCase
data("positive + positive", [3, 1, 2])
data("negative + negative", [-3, -1, -2])
data("positive + negative", [-1, 1, -2]) # 追加
data("negative + positive", [1, -1, 2]) # 追加
def test_add(data)
expected, augend, addend = data
assert_equal(expected, my_add(augend, addend))
end
end
データ表は次のようになります。
ラベル | expected |
augend |
addend |
---|---|---|---|
"positive + positive" |
3 |
1 |
2 |
"negative + negative" |
-3 |
-1 |
-2 |
"positive + negative" |
-1 |
1 |
-2 |
"negative + positive" |
1 |
-1 |
2 |
データ表生成機能を使うと次のように書けます。data
の第一引数にSymbol
を指定しているところがポイントです。テストに渡されるデータはHash
になっていてキーがシンボルで値が対象データです。
require "test-unit"
class TestAddDataMatrix < Test::Unit::TestCase
data(:augend, [1, -1])
data(:addend, [2, -2])
def test_add(data)
augend = data[:augend]
addend = data[:addend]
assert_equal(augend + addend,
my_add(augend, addend))
end
end
これで次のデータ表を生成できます。
ラベル | augend |
addend |
備考 |
---|---|---|---|
"addend: 2, augend: 1" |
2 |
1 |
正+正 |
"addend: 2, augend: -1" |
2 |
-1 |
正+負 |
"addend: -2, augend: 1" |
-2 |
1 |
負+正 |
"addend: -2, augend: -1" |
-2 |
-1 |
負+負 |
期待する結果(expected
)は生成できないのでRuby組み込みのInteger#+
の結果を使っています。これは実は大事なポイントです。データ表生成機能を使えるのは次の場合だけです。
-
期待する結果がデータに依らず一意に定まる
-
データから期待する結果を計算できる
今回の場合は期待する結果を計算できるので使えました。
なお、期待する結果は必ずしも正しい結果を返すはずの既存の実装(今回の場合はInteger#+
)を使わなくても大丈夫です。次のように「エンコードしてデコードしたら元に戻る」ようなときでもデータ表生成機能を使えます。これは性質をテストしているケースです。(性質をテストすることについてはここを参照してください、とか書いておきたいけど、どこがいいかしら。)
assert_equal(raw_data,
decode(encode(raw_data)))
この例ではパラメーターはaugend
とaddend
の2つでそれぞれに正と負があるので、4パターンでしたが、パラメーター数が増えたりバリエーションが増えると一気にパターンが増えます。そのときはこのデータ表生成機能が便利です。
なお、この機能はRed Chainer(Rubyだけで実装しているディープラーニングフレームワーク)で使うために作りました。もともとRed Chainerのテスト内でデータ表を生成していたのですがこの機能を使うことでだいぶスッキリしました。
データを使い回す
実はRed Chainerのテストをスッキリさせるためにはデータ表を生成するだけでは機能が足りませんでした。同じデータ表を複数のテストで共有する機能が必要でした。
前述の例で言うと、同じデータ表を足し算のテストでも引き算のテストでも使いたいという感じです。コードで言うと、以下をもっといい感じに書きたいということです。
require "test-unit"
class TestCalc < Test::Unit::TestCase
data(:number1, [1, -1])
data(:number2, [2, -2])
def test_add(data)
number1 = data[:number1]
number2 = data[:number2]
assert_equal(number1 + number2,
my_add(number1, number2))
end
data(:number1, [1, -1])
data(:number2, [2, -2])
def test_subtract(data)
number1 = data[:number1]
number2 = data[:number2]
assert_equal(number1 - number2,
my_subtract(number1, number2))
end
end
そこで、data
メソッドにkeep: true
オプションを追加しました。これで一度data
を書けば後続するテストでも同じデータを使うようになります。
require "test-unit"
class TestCalc < Test::Unit::TestCase
data(:number1, [1, -1], keep: true) # keep: trueを追加
data(:number2, [2, -2], keep: true) # keep: trueを追加
def test_add(data)
number1 = data[:number1]
number2 = data[:number2]
assert_equal(number1 + number2,
my_add(number1, number2))
end
# ここにdataはいらない
def test_subtract(data)
number1 = data[:number1]
number2 = data[:number2]
assert_equal(number1 - number2,
my_subtract(number1, number2))
end
end
データ表を複数生成する
実はRed Chainerのテストをスッキリさせるためにはデータを使い回せても機能が足りませんでした。1つのテストに対して複数のデータ表を生成する機能が必要でした。
前述の例で言うと、小さい数同士と大きい数同士で別のデータ表を作りたい、ただし、小さい数と大きい数の組み合わせはいらないという感じです。(わかりにくい。)
データ表で言うと次の2つのデータ表を使う感じです。
小さい数用のデータ表:
内容 | augend |
addend |
---|---|---|
小さい正 + 小さい正 | 2 |
1 |
小さい正 + 小さい負 | 2 |
-1 |
小さい負 + 小さい正 | -2 |
1 |
小さい負 + 小さい負 | -2 |
-1 |
大きい数用のデータ表:
内容 | augend |
addend |
---|---|---|
大きい正 + 大きい正 | 20000 |
10000 |
大きい正 + 大きい負 | 20000 |
-10000 |
大きい負 + 大きい正 | -20000 |
10000 |
大きい負 + 大きい負 | -20000 |
-10000 |
コードで言うと、以下をもっといい感じに書きたいということです。
require "test-unit"
class TestAdd < Test::Unit::TestCase
data(:augend, [1, -1])
data(:addend, [2, -2])
def test_add_small(data)
augend = data[:augend]
addend = data[:addend]
assert_equal(augend + addend,
my_add(augend, addend))
end
data(:augend, [10000, -10000])
data(:addend, [20000, -20000])
def test_add_large(data)
augend = data[:augend]
addend = data[:addend]
assert_equal(augend + addend,
my_add(augend, addend))
end
end
そこで、data
メソッドにgroup:
オプションを追加しました。同じグループ毎にデータ表を生成します。
require "test-unit"
class TestAdd < Test::Unit::TestCase
data(:augend, [1, -1], group: :small) # 小さい数用
data(:addend, [2, -2], group: :small) # 小さい数用
data(:augend, [10000, -10000], group: :large) # 大きい数用
data(:addend, [20000, -20000], group: :large) # 大きい数用
def test_add(data)
augend = data[:augend]
addend = data[:addend]
assert_equal(augend + addend,
my_add(augend, addend))
end
end
setup
でもデータを参照可能にする
実はRed Chainerのテストをスッキリさせるためにはデータ表を複数作れても機能が足りませんでした。テスト実行中にデータを参照しやすくする機能が必要でした。
従来のデータ駆動テスト機能ではテストメソッドの引数でデータを渡していました。そのため、setup
中でデータを参照できませんでした。
require "test-unit"
class TestAdd < Test::Unit::TestCase
def setup
# ここでデータを参照できない
end
data(:augend, [1, -1])
data(:addend, [2, -2])
def test_add(data)
augend = data[:augend]
addend = data[:addend]
assert_equal(augend + addend,
my_add(augend, addend))
end
end
Red Chainerのテストではデータを前処理したかったので次のように明示的に前処理メソッドを呼んでいました。
require "test-unit"
class TestAdd < Test::Unit::TestCase
def my_setup(data)
# 前処理
end
data(:augend, [1, -1])
data(:addend, [2, -2])
def test_add(data)
my_setup(data)
augend = data[:augend]
addend = data[:addend]
assert_equal(augend + addend,
my_add(augend, addend))
end
end
これは微妙なのでdata
でデータを参照できるようにしました。
require "test-unit"
class TestAdd < Test::Unit::TestCase
def setup
p data # データを参照できる!
end
data(:augend, [1, -1])
data(:addend, [2, -2])
def test_add(data)
augend = data[:augend]
addend = data[:addend]
assert_equal(augend + addend,
my_add(augend, addend))
end
end
また、テストでも引数でデータを受け取らなくてもよくなりました。(従来どおり受け取ってもよいです。)
require "test-unit"
class TestAdd < Test::Unit::TestCase
def setup
p data # データを参照できる!
end
data(:augend, [1, -1])
data(:addend, [2, -2])
def test_add # test_add(data)としなくてもよい!
augend = data[:augend]
addend = data[:addend]
assert_equal(augend + addend,
my_add(augend, addend))
end
end
まとめ
Red Chainerのためにtest-unitにデータ表生成機能を追加しました。Ruby 2.6.0にもこの機能を使えるtest-unitが入っています。ぜひ活用してください。
なお、Ruby 2.6.0でなくてもRubyGemsで新しいtest-unit(3.2.9以降)にアップグレードすれば使えます。Red Chainerでもそうやって使っています。
Red Chainerの開発に参加したい人はRed Data Toolsに参加してください。オンラインのチャットか東京で毎月開催している開発の集まり(次回は2018年1月22日)でどうやって進めていくか相談しましょう。