🦀

Rust でパラメタライズドテスト

2 min read 5

今回、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行に圧縮された上、テストコードの意図が読み取りやすくなった。

https://github.com/tamanobi/rusty-journal/commit/baff80a05e19513a78316df3342b21c308ea6ddb