🛡️

【Rust】ユニットテストであそぼう!

2023/10/01に公開2

はじめに

Rustのユニットテストに関する投稿です。
初学者向けの記事となります。

ユニットテストとは?

テストは、テスト以外のコードが想定通りに動いているかを確かめるRustの関数です。一般にテスト関数は、準備をしてからテストしたいコードを実行し、そしてその結果が期待したものであるか確認します。

https://doc.rust-jp.rs/rust-by-example-ja/testing/unit_testing.html

。。。



ざっくり言うと、自分の作ったプログラムの部品が、問題なく動いているかチェックすることです。そのチェックの手段がテストコードとなり、テストコードを書いてそれを実行しましょう!ということです。

実際に書いていきます。

テストコードを書いてみる

まずは前回のソースコードを使います。

https://zenn.dev/collabostyle/articles/3b0d0c9c48cb41

このコードにメソッドを2つ追加します。ボーカロイドが大人かチェックするメソッドと、こんにちは!と言うメソッドを追加します。

#[derive(Debug)]
struct Vocaloid {
    name: String,
    age: u8,
    songs: Vec<String>,
}

impl Vocaloid {
    fn new(name: String, age: u8, songs: Vec<String>) -> Self {
        Self { name, age, songs }
    }

    fn info(&self) {
        println!("vocaloid info: {:#?}", &self);
    }

+    fn is_adult(&self) -> bool {
+        self.age >= 18
+    }

+    fn say_hello(&self) -> String {
+        format!("Hello {}!", self.name)
+    }
}

pub fn run() {
    let songs = vec![
        String::from("The Vampire"),
        String::from("Cinderella"),
        String::from("Ruma"),
    ];
    let miku = Vocaloid::new(String::from("Hatsune Miku"), 16, songs);
}

この追加したコードに対し、テストコードを記述します。テストアフター開発

まずは同一ファイル内にサブモジュールを追加します。

mod test {
}

そして、アトリビュートを追加します。

+ #[cfg(test)]
mod test {
}

親クラスのすべての関数を対象にします。

#[cfg(test)]
mod test {
+    use super::*;
}

大人かどうかをチェックするメソッドに対し、テストコードを追加します。

#[cfg(test)]
mod test {
    use super::*;

+    #[test]
+    fn test_is_not_adult() {
+        let miku = Vocaloid::new(String::from("Hatsune Miku"), 16, vec![String::from("Ruma")]);
+        assert!(miku.is_adult());
+    }
}

assert! は期待どおりに返ってきているかをチェックするマクロです。
ここで注意しないといけないのは、ミクさんは16歳のため大人ではないということです。
つまり、実行すると false となります。

以下のコマンドでテストコードを実行します。

$ cargo test

結果、レッドで返ってきました。

これをレッドからグリーンにします。(ちなみに、テスト駆動開発では先にテストコードを書くため、対象のコードはありません。最初はレッドになります。レッドからグリーンです。レガシーコードからの脱却より

大人ではないことを確認するためのテストコードなので、! をつけて false を期待値にします。

- assert!(miku.is_adult());
+ assert!(!miku.is_adult());

再度テストコードを実行します。結果、グリーンで返ってきました。

続いて、大人である場合のテストコードを追加します。

#[test]
fn test_is_adult() {
    let kaito = Vocaloid::new(String::from("KAITO"), 26, vec![String::from("Ohed")]);
    assert!(kaito.is_adult());
}

テストコードを実行します。結果、2件グリーンで返ってきます。

次に、こんにちは!と言うメソッドに対し、戻り値に名前が含まれているかをテストします。以下のコードを追加します。(モックが冗長になっていますが)

#[test]
fn test_say_hello_contains_name() {
    let miku = Vocaloid::new(String::from("Hatsune Miku"), 16, vec![String::from("Ruma")]);
    assert!(miku.say_hello().contains("Hatsune Miku"));
}

#[test]
fn test_say_hello_does_not_contain_different_name() {
    let miku = Vocaloid::new(String::from("Hatsune Miku"), 16, vec![String::from("Ruma")]);
    assert!(!miku.say_hello().contains("KAITO"));
}

テストを実行します。

無事、グリーンが4件返ってきました。

さいごに

今回はユニットテストについての説明でしたが、登場したコードの中に冗長的なコードがありました。
once_cell あたりを使うと問題を回避できそうです。

Discussion

白山風露白山風露

重い処理を一回しか実行したくないとか、グローバルで状態を管理する必要があるとかなら once_cell の出番ですが、このくらいのテストのセットアップでわざわざグローバルな状態を作り出さない方がいいのではないでしょうか。コードの冗長性を減らしたいなら

fn new_mock(): Vocaloid {
    Vocaloid::new(String::from("Hatsune Miku"), 16, vec![String::from("Ruma")])
}

みたいな共通化をするだけでいいと思います。

はむるはむる

白山さんコメントありがとうございます。
おっしゃるとおり、今回の例だとただ関数化するだけでよいですね。