🦀

The Rust Programing Language 9日目

2023/11/08に公開

前回のあらすじ

前回はジェネリクス、Trait、ライフタイムについて学んだ
ざっと理解できたが、怪しくなり始めてきた気がする
読めるけど書けない状態ではある

余談

Zennにはスクラップというものと記事というものがあるが、スクラップて何?
この連載はスクラップにすべきなのか?
あと書き方は日々改善を重ねているので統一感とかは無い
TRPLを読みながら雑に書いて、翌日記事を読み直す事で簡易的な復習にしているのでその時読みにくいと思ったらその日書く分は改善する、戻って直しはしない

あと、いいねくれたりした方がいて非常に励みになりました。
特に参考になるような内容ではないですが、圧倒的感謝・・・!

本日の学び

本日は11章 自動テストを書くを読む

Rustにおけるテスト

事前知識

どうやらRustはテストを外部ライブラリを使用せず書けるらしい!おわり

テストの記述方法

最もシンプルなRustにおけるテストは、test属性で注釈された関数のこと

#[test]
fn it_works() {
    assert_eq!(2 + 2, 4);
}

こんな感じ

新しいライブラリプロジェクトをCargoで作成すると、テストモジュールが自動生成される

$ cargo new adder --lib
     Created library `adder` project
$ cd adder
src/lib.rs
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

testsモジュール内にはテスト関数以外の関数を作成し、テストのセットアップなどを行える
そのため、どの関数がテストなのかを注釈で示す必要がある

cargo testでテストを走らせる

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.57s
     Running target/debug/deps/adder-92948b65e88960b4

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

measuredはベンチマークテスト用で、TRPL内では解説しない。
Doc-testsはドキュメンテーションテストの結果で、これは後々の章で解説。

失敗するテスト

テスト関数内でpanicするとテストは失敗する

src/lib.rs
#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        //このテストを失敗させる
        panic!("Make this test fail");
    }
}
$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.72s
     Running target/debug/deps/adder-92948b65e88960b4

running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok

failures:

---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.


failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--lib'

assertする

assert!マクロでアサーションが可能
assert!結果がfalseなら、内部でpanic!を呼び出し、テストは失敗する
assert_eq!assert_ne!マクロで、2値が等しいか(等しくないか)をテスト可能

失敗メッセージをカスタムする

assert系のメソッドの必須引数の後に追加した引数は全てformat!マクロに渡される

    fn greeting_contains_name() {
        let result = greeting("Carol");
        assert!(
            result.contains("Carol"),
            //挨拶(greeting)は名前を含んでいません。その値は`{}`でした
            "Greeting did not contain name, value was `{}`",
            result
        );
    }
$ cargo test
   Compiling greeter v0.1.0 (file:///projects/greeter)
    Finished test [unoptimized + debuginfo] target(s) in 0.93s
     Running target/debug/deps/greeter-170b942eb5bf5e3a

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--lib'

失敗した時のメッセージを変更する意味ある?と思ったが、確かに変数の中身を表示するのは有用か

panicを確認する

テスト関数にshould_panicという別の属性を追加することで、想定通りpanicしていることを確認する

src/lib.rs
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            //予想値は1から100の間でなければなりませんが、{}でした。
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

should_panic属性にexpected引数を追加することで、エラーメッセージの想定を追加できる

src/lib.rs
// --snip--
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                //予想値は1以上でなければなりませんが、{}でした。
                "Guess value must be greater than or equal to 1, got {}.",
                value
            );
        } else if value > 100 {
            panic!(
                //予想値は100以下でなければなりませんが、{}でした。
                "Guess value must be less than or equal to 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

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

    #[test]
    //予想値は100以下でなければなりません
    #[should_panic(expected = "Guess value must be less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

Javaなどだと、throwされた例外クラスを判定できるのにと思ったが、それはResultが相当するのかな
まああまりそういうテストを書いたことはないですが。。

Result<T, E>をテストで使う

panicする代わりにErrを返すようにしてテストを書くことも可能

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

Result<T, E> を返すようなテストを書くと、?演算子をテストの中で使えるようになって便利な時もある

テストの実行方法を制御する

並行or連続

通常、テストは並行で走るが、テストの実行順が意味を持つ場合などはスレッド数を指定することができる
$ cargo test -- --test-threads=1
上記は並行実行しないように指定する

関数の出力を表示する

通常、テスト中に実行されたpringln!などの出力は、キャプチャされテスト結果として表示されない
$ cargo test -- --nocaptureで実行することで、表示させることができる

一部のテストだけを実行する

  • 関数名をcargo testに渡して、直接そのテストのみを実施することができる
  • 関数名の一部を渡すことで、部分一致で該当するテストのみが実行される

基本的に無視するテストを作成する

非常に重たいテストなど、毎回は実行したくないものについてはignore属性で除外することができる
(ignore、昔いたJavaプロジェクトではデータが壊れて動かなくなったテストに使ったな。。)
cargo test -- --ignoredとすると、ignoredのテストのみを実行できる
ignoredのものも含めて全部実行するにはどうしたら良いのでしょうか?
-> ちゃんと調査してないけどパッと調べた感じcargo test -- --include-ignoredでできそう?
後日検証しよう

テストの体系化

単体テスト・結合テスト

単体テストは各ファイルにテスト対象コードと共に置くらしい、マジ?
まあその方がわかりやすいか・・・
業務ロジックとテストコードが同居してるのはなんとなく違和感があるが、モジュールに注釈しておけばコンパイルされないんだからいいのか
結合テストはライブラリ外になり、完全に外部から使用するのと同様に複数もモジュールに跨りテストする

テストモジュールと#[cfg(test)]

#[cfg(test)]という注釈は、cargo buildではなくcargo test時のみコンパイルするという指定

結合テスト

結合テストはtestsディレクトリに作成する
これはCargoも認識している
この中にテストファイルを自由に作成でき、cargoはそれぞれを個別のクレートとしてコンパイルする

ちょっとこの後読み飛ばします、結合テストが必要になるのはまだ先だと思うので。。。
今読んでもどうせ忘れるので、いつの日か結合テストを実装するときにここに帰ってきます。

本日のまとめ

assertionの手法、単体テストの慣習を学んだ
通して思ったのは、非常にTDDと相性が良いのでは?というやつ
cargo new時にtestが生成されるのはTDDしろという哲学ではないのか?違う?
実装する時は試してみたいと思った

明日は(元気なら)12章、コマンドラインプログラム作成
今日は週の中日に酒飲んで帰ってきてこれ書いてるので偉さ1億点くらいある

Discussion