💤

rustでユニットテストを別ファイルで管理したい

2024/10/18に公開1

rustではユニットテストはテスト対象と同じファイルに記述するのがならわしのようです。同一ファイルに書かなくても一応テスト自体はできますが、privateな識別子にアクセスできないといったデメリットが発生します。

しかし、テストコードは大体テスト対象のコードの3 ~ 5倍くらいのコード量になるのなんて普通なので、そんな規模感のコードがプロダクトコードと同じファイルに書かれてるとgrepしづらいやらdiffも見づらいやらでデメリットしかない気がしますね。同じファイルを上下左右に分割してコード書くのもやりにくいです。
そんなワケなのでprivateな識別子にアクセスできる状態でユニットテストを別ファイルに分割して管理することが求められます。私の中で。なのでやりましょう。

一体どうすれば?

要はコンパイルの時点でテストコードがテスト対象のコードと同じファイルの中に記述されていればいいってことです。
これを実現するためにはマクロを作って呼び出してしまえばいいのですね。

hoge.rs
struct Hoge {
}

impl Hoge {

    fn do_something(&self) -> i32 {
        10
    }
}

#[cfg(test)]
crate::hoge_test::define_hoge_test!();
mod.rs
mod hoge;

#[cfg(test)]
mod hoge_test;
hoge_test.rs
#[macro_export]
macro_rules! define_hoge_test {
    () => {
        mod tests {

            use crate::hoge::Hoge;

            #[test]
            fn test_do_somthing() {
                assert_eq!(Hoge{}.do_something(), 10);
            }
        }
    }
}

pub use define_hoge_test;

これでいけます。しかし、これだと流石に不恰好すぎるというか、ネストが深すぎて視認性も悪いのでなんか嫌ですね。

includeマクロを使おう

こんなことのためなのか全くわかりませんが、rustにはincludeという指定したファイルを呼び出し元にそのまま展開できる機能があります。c言語のincludeと同じような機能です。
これを使えばごちゃごちゃしたマクロを書かずに普通にテストが記述できます。mod.rs内部での宣言も不要です。

hoge.rs
struct Hoge {
}

impl Hoge {

    fn do_something(&self) -> i32 {
        10
    }
}

#[cfg(test)]
include!("./hoge_test.rs");
hoge_test.rs
mod tests {

    use crate::hoge::Hoge;

    #[test]
    fn test_do_somthing() {
        assert_eq!(Hoge{}.do_something(), 10);
    }
}

以上になります。

Discussion

anatawa12anatawa12

rustではユニットテストはテスト対象と同じファイルに記述するのがならわしのようです。

これには少し誤認があるように思えます。

pubでないものをアクセスする都合等もあり、同じモジュール内にあることは要求されていてまたそのようにすることが多いですが、入れ子のmodが別ファイルで書かれることも多くあると思います。

以下はrust stdの例です。このようにf64.rsのテストがf64/tests.rsに書かれています

https://github.com/rust-lang/rust/blob/1.82.0/library/std/src/f64.rs#L15-L16
https://github.com/rust-lang/rust/blob/1.82.0/library/std/src/f64/tests.rs