The Rust Programing Language 9日目
前回のあらすじ
前回はジェネリクス、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
#[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するとテストは失敗する
#[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していることを確認する
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
引数を追加することで、エラーメッセージの想定を追加できる
// --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