Charaken 技術系ブログ

技術系に関して学んだことや、気になったことを記事にしていく。

【Rust】がばがばRust独学 - 11. Tests - 1 How to Write Tests

f:id:charaken:20191223210559p:plain

Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)

doc.rust-lang.org

今回は、Testについて学びつつ、気になった点をコード確認した結果を記事にしています。

  • versions
$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)


How to Write Tests

公式ドキュメント(How to Write Tests - The Rust Programming Language)によると、テストは3つのアクションを持っているそうです。

  1. Set up any needed data or state.
  2. Run the code you want to test.
  3. Assert the results are what you expect.

必要なデータ・状態を設定し、テストコードを実行し、結果が期待通りで有ることをアサートします。

テスト関数の構造

テスト関数を実施するために、とりあえず新しいライブラリプロジェクトを作成しました。

github.com

$ cargo new demo-test --lib
$ demo-test

テストコードについては、src/lib.rstraining-rust/lib.rs at master · KentaHara/training-rust · GitHub)を参照ください。

まず、初期で記載されているテストを実行すると、正常動作が確認できました。

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}
running 1 test
test tests::it_works ... ok

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

別名関数を作成して実行すると、各々 tests::xxxxx としてチェックされていることが確認できました。

    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }
running 2 tests
test tests::exploration ... ok
test tests::it_works ... ok

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

panicを起こすと FAILED となり、異常検知が出来ていることが確認できます。

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
running 3 tests
test tests::exploration ... ok
test tests::it_works ... ok
test tests::another ... FAILED

failures:

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


failures:
    tests::another

test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
assert! マクロ利用によるテスト結果確認

デモコード(training-rust/lib.rs at master · KentaHara/training-rust · GitHub)のように、 Rectangle の構造体および can_hold 関数を定義して、 assert! マクロによるチェックを実施しました。 assert! マクロは bool によるチェックになります。

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

// ...

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

    // ...

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }
    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}
running 5 tests
...
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
assert_eq!assert_ne! によるテスト

it_adds_two では 4 == 4it_adds_two_negative では 5 == 4 としてテストを実施しました。

pub fn add_two(a: i32) -> i32 {
    a + 2
}

// ...

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

    // ...

    #[test]
    fn it_adds_two() {
        assert_eq!(4, add_two(2));
    }

    #[test]
    fn it_adds_two_negative() {
        assert_ne!(5, add_two(2));
    }
}
running 7 tests
...
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
テスト失敗時のメッセージ変更

assert! の第2引数と第3引数に対してフォーマットの利用が可能です。これは便利...

    #[test]
    fn change_error_message() {
        let result = String::from("Hello Rust!");
        assert!(
            result.contains("Hello New World!"),
            "result did not contain name, value was `{}`",
            result
        );
    }
    #[test]
    fn change_error_message_use_eq() {
        let result = String::from("Hello Rust!");
        assert_eq!(
            result,
            String::from("Hello New World!"),
            "result did not contain name, value was `{}`",
            result
        );
    }
    #[test]
    fn change_error_message_use_ne() {
        let result = String::from("Hello Rust!");
        assert_ne!(
            result,
            String::from("Hello Rust!"),
            "result did not contain name, value was `{}`",
            result
        );
    }
running 8 tests
...
test tests::change_error_message ... FAILED
...

failures:

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


failures:
    tests::change_error_message

test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
panicのチェック

シンプルにパニックを起こす関数 simple_panic を定義して確認すると、 should_panic である場合はpanicが見つけられなければ FAILED となる。

pub fn simple_panic(flg: bool) -> bool {
    if flg {
        panic!("Panic!!");
    }
    true
}
#[cfg(test)]
mod tests {
    use super::*;
    // ...

    #[test]
    #[should_panic]
    fn should_panic_test_panic() {
        simple_panic(true);
    }

    #[test]
    #[should_panic]
    fn should_panic_test_nopanic() {
        simple_panic(false);
    }
}
running 12 tests
...
test tests::should_panic_test_nopanic ... FAILED
test tests::should_panic_test_panic ... ok

failures:

...

---- tests::should_panic_test_nopanic stdout ----
note: test did not panic as expected

failures:
    tests::change_error_message
    tests::change_error_message_use_eq
    tests::change_error_message_use_ne
    tests::should_panic_test_nopanic

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

上記の場合は、 note: test did not panic as expected として注意書きがされる。

Result<T,E> の利用

Resultを返り値として返すこともでき、Errの場合にはエラーメッセージを出すことも可能です。

    #[test]
    fn it_works_use_result_ok() -> Result<(), String> {
        Ok(())
    }

    #[test]
    fn it_works_use_result_err() -> Result<(), String> {
        Err(String::from("ERROR MESSAGE"))
    }
---- tests::it_works_use_result_err stdout ----
Error: "ERROR MESSAGE"
thread 'tests::it_works_use_result_err' panicked at 'assertion failed: `(left == right)`
  left: `1`,
 right: `0`: the test returned a termination value with a non-zero status code (1) which indicates a failure', src/libtest/lib.rs:196:5

まとめ

github.com

  • mod には #[cfg(test)] を付与してtestということをconfigとして設定
  • テスト用の関数には #[test] を利用
    • 必ずpanicになるか否かを確認するためには #[should_panic] を利用
  • 各種マクロによりテスト結果への対応可能
    • assert_eq!(a, b)a == b
    • assert_en!(a, b)a != b
    • assert!(x)x == true
  • 各種マクロの引数を利用してテスト失敗時のメッセージを変更可能
  • Result<T,E> による成否のチェックも可能

最後に

シンプルなテストの記述方法で書きやすそうですね。 Result<T,E> を使える点がとてもありがたいです。

次回も引き続き、Testsに関して進めていきます。