Rust でパラメタライズドテスト
今回、Rustでテストコードを書いていて、パラメタライズドテストを省エネで書きたくなった。テストコードにしたって、何度も同じタイピングなんてしたくない。コピペなんて言語同断。
かなしいコード
例えば、こんなコード。
pub fn add(a:i32, b:i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
#[test]
fn test_add_1_1_returns_2() {
assert_eq!(add(1, 1), 2);
}
#[test]
fn test_add_0_100_returns_100() {
assert_eq!(add(0, 100), 100);
}
}
テストケースごとに似たような見た目のテストコードが増えていくことが目に見えている。
Go言語では、テストケースを構造体にまとめて、テストコードを見やすくすることを推奨している。
GitHub の Wiki にテーブル駆動テストという項目に掲載されている。
また、Python の標準ライブラリの一つである unittest にも SubTest という似たような仕組みがある。
一方、Rustにはそのような仕組みがない。Rustは標準ライブラリが小さいことがよく知られているので仕方ない。だからといって、テストコードをコピペしてみっともない真似はしたくない。
Crates.io から探さずに自作する
プログラミング言語には、標準ライブラリ以外にサードパーティライブラリがあるため、さほど困らない。Rust には crates.io がある。JUnit にインスパイアされたという parameterized というライブラリを使うのが簡単そうだ。
Go や Python の例をみるとお分かりのとおり、シンプルな実装でできそうな気がしてくる。
Rust には強力な macro 機能がある。
マクロで作ってみた
#[cfg(test)]
mod tests {
macro_rules! test_add {
($($name:ident: $value:expr, )*) => {
$(
#[test]
fn $name() {
let (lhs, rhs, expected) = $value;
assert_eq!(crate::sample::add(lhs, rhs), expected);
}
)*
};
}
test_add! {
add1: (1, 1, 2),
add2: (0, 100, 100),
}
}
macro_rules を使って test_add
マクロを生成する。今回は シンプルなコードなのでマクロを作る手間のほうが大きいが、このコードがサクッとかけるとライブラリに頼ることなくコードがかける。
関数単位のテストケースをマクロで生成しているので、テストが失敗してもほかのテストケースのテストは続行される。
なぜライブラリに頼らないか
ライブラリは便利だが、たくさんインストールすると依存性の解消や、脆弱性への対応などメンテナンスコストが高くなる。
ライブラリ選定に時間をかけるより、自分で書いたほうが早いということもある。盲目的に選択肢を絞るのではなく、いつでも多くの選択肢を見つけられるようにしておくとよい。
※とは言っても、マクロはデバッグが大変になるのであまりお勧めはしない。
実際にマクロを使ってコードを圧縮した事例
37行のコードが18行に圧縮された上、テストコードの意図が読み取りやすくなった。
Discussion
テーブルで作ってみた
Go方面からきた私が通りますよ〜
この作法はBDDのDescribeTableが由来な気がしますね。
テーブル+ジェネリクスで作ってみた
@kishiguro さん、コメントとコードありがとうございます! 大変参考になります。
いま出先で確認できないのですが、おそらく kishiguro さんのコードだと test_add 実行中に add がパニックを起こしたとき、
tests
のテストケース途中で止まってしまいそうです。私の書いたマクロでは、関数をマクロで生成されているので、テストケースごとに関数が作られる仕組みになっています。(文中の「関数単位のテストケースをマクロで生成しているので……」という箇所)
Goでいう、 testing.T のような存在が Rust にあれば、それを使ってパニックを処理できそうに思えます。何かご存知でしょうか?
そうですね。assertでパニックした場合はそこで、test_add()止まっちゃいますね。止めたくない場合はやっぱり@tamanobiさんのようにマクロで書くのがいいと思います。
テーブルで書くメリットはその一覧性によって、人間がテストケース全体を一眼で把握できることにあるとすると、マクロで書くとしても、もうちょっと工夫したくなりますね。
@kishiguro さん、ありがとうございます! これ以上マクロを工夫しようとすると、短時間で書けなさそう&メンテナンスコストが高くなりそうなので、素直にクレートを使おうかなと思います!
forループ等により複数のassertionを繰り返すのは、(テストフレームワークにもよりますが)以下のような欠点があるため、Assertion Rouletteというアンチパターンだと言われています.
なので、マクロを使ったやり方の方が、その点では優れているのだと思います.