テストの記述法
テストは、テスト以外のコードが想定された方法で機能していることを実証するRustの関数です。 テスト関数の本体は、典型的には以下の3つの動作を行います:
- 必要なデータや状態をセットアップする。
- テスト対象のコードを走らせる。
- 結果が想定通りであることを断定(以下、アサーションという)する。
Rustが、特にこれらの動作を行うテストを書くために用意している機能を見ていきましょう。
これには、test属性、いくつかのマクロ、should_panic属性が含まれます。
テスト関数の構成
最も単純には、Rustにおけるテストはtest属性で注釈された関数のことです。属性とは、
Rustコードの部品に関するメタデータです; 一例を挙げれば、構造体とともに第5章で使用したderive属性です。
関数をテスト関数に変えるには、fnの前に#[test]を付け加えてください。
cargo testコマンドでテストを実行したら、コンパイラは注釈された関数を走らせるテスト用バイナリをビルドし、
各テスト関数が通過したか失敗したかを報告します。
新しいライブラリプロジェクトをCargoで作ると、テスト関数付きのテストモジュールが自動的に生成されます。 このモジュールがテストを書くためのテンプレートを提供してくれるので、 新しいプロジェクトを始めるたびに正しい構造とか文法をいちいち検索しなくてすみます。 ここに好きな数だけテスト関数やテストモジュールを追加すればいいというわけです!
まずは実際にコードをテストする前に、自動生成されたテンプレートのテストで実験して、テストの動作の性質をいくらか学びましょう。 その後で、以前書いたコードを呼び出し、振る舞いが正しいことをアサーションする、ホンモノのテストを書きましょう。
2つの数を足す、adderという新しいライブラリプロジェクトを生成しましょう:
$ cargo new adder --lib
Created library `adder` project
$ cd adder
adderライブラリのsrc/lib.rsファイルの中身は、リスト11-1のような見た目のはずです。
ファイル名: src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
リスト11-1: cargo newで自動生成されたテストモジュールと関数
とりあえず、最初の2行は無視し、関数に集中しましょう。
#[test]注釈に注目してください: この属性は、これがテスト関数であることを示すので、
テスト実行機はこの関数をテストとして扱うとわかるのです。さらに、testsモジュール内にはテスト関数以外の関数を入れ、
一般的なシナリオをセットアップしたり、共通の処理を行う手助けをしたりもできるので、
必ずどの関数がテストかを示す必要があるのです。
例の関数本体は、assert_eq!マクロを使用して、2と2を足した結果を含むresultが、4に等しいことをアサーションしています。
このアサーションは、典型的なテストのフォーマット例をなしているわけです。走らせてこのテストが通る(訳注:テストが成功する、の意味。英語でpassということから、このように表現される)ことを確かめましょう。
cargo testコマンドでプロジェクトにあるテストが全て実行されます。リスト11-2に示したようにですね。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (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; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
リスト11-2: 自動生成されたテストを走らせた出力
Cargoがテストをコンパイルし、走らせました。running 1 testという行が見えます。
その次の行は、生成されたテスト関数のit_worksという名前と、このテストの実行結果がokであることを示しています。
テスト全体のまとめであるtest result:ok.は、全テストが通ったことを意味し、
1 passed; 0 failedと読める部分は、通過または失敗したテストの数を合計しているのです。
特定の場合にテスト実行しないように、テストを無視するように指定することができます;
これについては後でこの章の「特に要望のない限りテストを無視する」節で扱います。
ここではそれを行っていないので、まとめは0 ignoredと示しています。
cargo test コマンドに引数を渡すことで、名前が文字列にマッチするテストのみを実行することもできます;
フィルタリングと呼ばれますが、これについては「名前でテストの一部を実行する」節で扱います。
また、実行するテストにフィルタをかけもしなかったので、まとめの最後に0 filtered outと表示されています。
0 measuredという統計は、パフォーマンスを測定するベンチマークテスト用です。
ベンチマークテストは、本書記述の時点では、nightly版のRustでのみ利用可能です。
詳しくは、ベンチマークテストのドキュメンテーションを参照されたし。
テスト出力の次の部分、つまりDoc-tests adderで始まる部分は、ドキュメンテーションテストの結果用のものです。
まだドキュメンテーションテストは何もないものの、コンパイラは、APIドキュメントに現れるどんなコード例もコンパイルできます。
この機能により、ドキュメントとコードを同期することができるわけです。ドキュメンテーションテストの書き方については、
第14章のテストとしてのドキュメンテーションコメント節で議論しましょう。今は、Doc-tests出力は無視します。
それでは必要に応じてテストをカスタマイズしていきましょう。
まずはit_works関数の名前を違う名前に、例えば以下のexplorationのように変更してください:
ファイル名: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn exploration() {
assert_eq!(2 + 2, 4);
}
}
そして、cargo testを再度走らせます。これで出力がit_worksの代わりにexplorationと表示しています:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.59s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
それでは別のテストを追加していきます。ただし、今回は失敗するテストにしましょう!
テスト関数内の何かがパニックすると、テストは失敗します。
各テストは、新規スレッドで実行され、メインスレッドが、テストスレッドが死んだと確認した時、テストは失敗と印づけられます。
第9章で、パニックを引き起こす最も単純な方法はpanic!マクロを呼び出すことだと語りました。
src/lib.rsファイルがリスト11-3のような見た目になるよう、anotherという名前の関数として新しいテストを入力してください。
ファイル名: src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn exploration() {
assert_eq!(2 + 2, 4);
}
#[test]
fn another() {
//このテストを失敗させる
panic!("Make this test fail");
}
}
リスト11-3: panic!マクロを呼び出したために失敗する2番目のテストを追加する
cargo testで再度テストを走らせてください。出力はリスト11-4のようになるはずであり、
explorationテストは通り、anotherは失敗したと表示されます。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
---- tests::another stdout ----
thread 'tests::another' panicked at src/lib.rs:10:9:
Make this test fail
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; finished in 0.00s
error: test failed, to rerun pass `--lib`
リスト11-4: 一つのテストが通り、一つが失敗するときのテスト結果
okの代わりにtest test::anotherの行は、FAILEDを表示しています。個々の結果とまとめの間に、
2つ新たな区域ができました: 最初の区域は、失敗したテスト各々の具体的な理由を表示しています。
今回の場合、anotherはsrc/lib.rsファイルの10行目で'Make this test fail'でパニックしたために失敗した、という詳細が得られました。
次の区域は失敗したテストの名前だけを列挙しています。
これは、テストがたくさんあり、失敗したテストの詳細がたくさん表示されるときに有用になります。
失敗したテストの名前を使用してそのテストだけを実行し、より簡単にデバッグすることができます。
テストの実行方法については、テストの実行され方を制御する節でもっと語りましょう。
サマリー行が最後に出力されています: 総合的に言うと、テスト結果はFAILEDでした。
一つのテストが通り、一つが失敗したわけです。
様々な状況でのテスト結果がどんな風になるか見てきたので、テストを行う際に有用になるpanic!以外のマクロに目を向けましょう。
assert!マクロで結果を確認する
assert!マクロは、標準ライブラリで提供されていますが、テスト内の何らかの条件がtrueと評価されることを確かめたいときに有効です。
assert!マクロには、論理値に評価される引数を与えます。その値がtrueなら、
何も起こらずにテストは通ります。その値がfalseなら、assert!マクロはpanic!を呼び出し、
テストは失敗します。assert!マクロを使用することで、コードが意図した通りに機能していることを確認する助けになるわけです。
第5章のリスト5-15で、Rectangle構造体とcan_holdメソッドを使用しました。リスト11-5でもそれを繰り返しています。
このコードをsrc/lib.rsファイルに放り込み、assert!マクロでそれ用のテストを何か書いてみましょう。
ファイル名: src/lib.rs
#[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
}
}
リスト11-5: 第5章からRectangle構造体とそのcan_holdメソッドを使用する
can_holdメソッドは論理値を返すので、assert!マクロの完璧なユースケースになるわけです。
リスト11-6で、幅が8、高さが7のRectangleインスタンスを生成し、これが幅5、
高さ1の別のRectangleインスタンスを保持できるとアサーションすることでcan_holdを用いるテストを書きます。
ファイル名: src/lib.rs
#[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));
}
}
リスト11-6: より大きな長方形がより小さな長方形を確かに保持できるかを確認するcan_hold用のテスト
testsモジュール内に新しい行を加えたことに注目してください: use super::*です。
testsモジュールは、第7章のモジュールツリーの要素を示すためのパス節で講義した通常の公開ルールに従う普通のモジュールです。
testsモジュールは、内部モジュールなので、外部モジュール内のテスト配下にあるコードを内部モジュールのスコープに持っていく必要があります。
ここではglobを使用して、外部モジュールで定義したもの全てがこのtestsモジュールでも使用可能になるようにしています。
テストはlarger_can_hold_smallerと名付け、必要なRectangleインスタンスを2つ生成しています。
そして、assert!マクロを呼び出し、larger.can_hold(&smaller)の呼び出し結果を渡しました。
この式は、trueを返すと考えられるので、テストは通るはずです。確かめましょう!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 1 test
test tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
通ります!別のテストを追加しましょう。今回は、小さい長方形は、より大きな長方形を保持できないことをアサーションします。
ファイル名: src/lib.rs
#[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() {
// --snip--
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));
}
}
今回の場合、can_hold関数の正しい結果はfalseなので、その結果をassert!マクロに渡す前に反転させる必要があります。
結果として、can_holdがfalseを返せば、テストは通ります。
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
通るテストが2つ!さて、コードにバグを導入したらテスト結果がどうなるか確認してみましょう。
幅を比較する大なり記号を小なり記号で置き換えてcan_holdメソッドの実装を変更しましょう:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// --snip--
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));
}
}
テストを実行すると、以下のような出力をします:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
(スレッド'tests::larger_can_hold_smaller'はsrc/lib.rs:28:9でパニックしました:
アサーション失敗: larger.can_hold(&smaller))
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
テストによりバグが捕捉されました!larger.widthが8、smaller.widthが5なので、
can_hold内の幅の比較が今はfalseを返すようになったのです: 8は5より小さくないですからね。
assert_eq!とassert_ne!マクロで等値性をテストする
機能性を検証する一般的な方法は、テスト下にあるコードの結果と、コードが返すと期待される値との、等値性を確かめることです。
これをassertマクロを使用して==演算子を使用した式を渡すことで行うこともできます。
しかしながら、これはありふれたテストなので、標準ライブラリには1組のマクロ(assert_eq!とassert_ne!)が提供され、
このテストをより便利に行うことができます。これらのマクロはそれぞれ、二つの引数を比べ、等しいかと等しくないかを確かめます。
また、アサーションが失敗したら二つの値の出力もし、テストが失敗した原因を確認しやすくなります。
一方でassert!マクロは、==式の値がfalseになったことしか示さず、falseになった原因の値は出力しません。
リスト11-7では、引数に2を加えるadd_twoという名前の関数を書いて、
この関数をassert_eq!マクロでテストしています。
ファイル名: src/lib.rs
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));
}
}
リスト11-7: assert_eq!マクロでadd_two関数をテストする
これが通ることを確認しましょう!
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
assert_eq!マクロに引数として4を渡していますが、これはadd_two(2)の呼び出し結果と等しいです。
このテストの行はtest tests::it_adds_two ... okであり、okというテキストはテストが通ったことを示しています!
コードにバグを仕込んで、assert_eq!が失敗した時にそれがどうなるのか確認してみましょう。
add_two関数の実装を代わりに3を足すように変えてください:
pub fn add_two(a: i32) -> i32 {
a + 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
}
テストを再度実行します:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at src/lib.rs:11:9:
assertion `left == right` failed
left: 4
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
テストがバグを捕捉しました!
it_adds_twoのテストは失敗し、そのメッセージは、
失敗したアサーションがassertion failed: `(left == right)`であったこと、
そしてleftとrightの値が何だったかを示しています。
このメッセージはデバッグを開始する助けになります:
left引数は4だったが、add_two(2)があるright引数は5でした。
実行中のテストが多数あるときは特に、この出力は役に立つだろうと想像できるでしょう。
言語やテストフレームワークによっては、等値性アサーション関数の引数をexpectedとactualと呼び、
引数を指定する順序が重要であることに注意してください。
ですがRustでは、これらはleftとrightと呼ばれ、期待する値とコードが生成する値を指定する順序は重要ではありません。
今回のテストのアサーションをassert_eq!(add_two(2), 4)と書くこともでき、
そうすると失敗メッセージは、同じくassertion failed: `(left == right)`を表示するでしょう。
assert_ne!マクロは、与えた2つの値が等しくなければ通り、等しければ失敗します。
このマクロは、値が何になるだろうか確信が持てないけれども、
確実にこの値にはなるべきでないとわかっているような場合に最も有用になります。例えば、
入力を何らかの手段で変え(て出力す)ることが保証されているけれども、入力の変え方がテストを実行する曜日に依存する関数をテストしているなら、
アサーションすべき最善の事柄は、関数の出力が入力と等しくないことかもしれません。
内部的には、assert_eq!とassert_ne!マクロは、それぞれ==と!=演算子を使用しています。
アサーションが失敗すると、これらのマクロは引数をデバッグフォーマットを使用してプリントするので、
比較対象の値はPartialEqとDebugトレイトを実装していなければなりません。
すべての組み込み型と、ほぼすべての標準ライブラリの型はこれらのトレイトを実装しています。
自分で定義した構造体やenumについては、
その型の値の等値性をアサーションするために、PartialEqを実装する必要があるでしょう。
それが失敗した時にその値をプリントできるように、Debugも実装する必要もあるでしょう。
第5章のリスト5-12で触れたように、どちらのトレイトも導出可能なトレイトなので、
これは通常、単純に構造体やenum定義に#[derive(PartialEq, Debug)]という注釈を追加するだけですみます。
これらやその他の導出可能なトレイトに関する詳細については、付録C、導出可能なトレイトをご覧ください。
カスタムの失敗メッセージを追加する
さらに、assert!、assert_eq!、assert_ne!の追加引数として、失敗メッセージと共にカスタムのメッセージが表示されるよう、
追加することもできます。必須引数の後に指定された引数はすべてformat!マクロに渡されるので、
(format!マクロについては第8章の+演算子、またはformat!マクロで連結節で議論しました)、
{}プレースホルダーを含むフォーマット文字列とこのプレースホルダーに置き換えられる値を渡すことができます。
カスタムメッセージは、アサーションがどんな意味を持つかドキュメント化するのに役に立ちます;
もしテストが失敗した時、コードにどんな問題があるのかをよりしっかり把握できるはずです。
例として、人々に名前で挨拶をする関数があり、関数に渡した名前が出力に出現することをテストしたいとしましょう:
ファイル名: src/lib.rs
pub fn greeting(name: &str) -> String {
format!("Hello {}!", name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
このプログラムの要件はまだ取り決められておらず、挨拶の先頭のHelloというテキストはおそらく変わります。
要件が変わった時にテストを更新しなくてもよいようにしたいと考え、
greeting関数から返る値と正確な等値性を確認するのではなく、出力が入力引数のテキストを含むことをアサーションするだけにします。
それではgreetingがnameを含まないように変更してこのコードにバグを仕込み、テストの失敗がデフォルトでどんな風になるのか確かめましょう:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
このテストを実行すると、以下のように出力されます:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
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; finished in 0.00s
error: test failed, to rerun pass `--lib`
この結果は、アサーションが失敗し、どの行にアサーションがあるかを示しているだけです。
失敗メッセージがgreeting関数からの値を出力していればより有用でしょう。
greeting関数から得た実際の値で埋められるプレースホルダーを含むフォーマット文字列からなるカスタムの失敗メッセージを追加してみましょう:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
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 unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
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; finished in 0.00s
error: test failed, to rerun pass `--lib`
実際に得られた値がテスト出力に表示されているので、起こると想定していたものではなく、 起こったものをデバッグするのに役に立ちます。
should_panicでパニックを確認する
戻り値を確認することに加えて、想定通りにコードがエラー状態を扱っていることを確認することが重要です。
例えば、第9章のリスト9-10で生成したGuess型を考えてください。Guessを使用する他のコードは、
Guessのインスタンスは1から100の範囲の値しか含まないという保証に依存しています。
その範囲外の値でGuessインスタンスを生成しようとするとパニックすることを確認するテストを書くことができます。
これは、テスト関数にshould_panicという属性を追加することで達成できます。
このテストは、関数内のコードがパニックする場合に通過します。つまり、
関数内のコードがパニックしなかったら、テストは失敗するわけです。
リスト11-8は、予想どおりにGuess::newのエラー条件が発生していることを確認するテストを示しています。
ファイル名: 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);
}
}
リスト11-8: 状況がpanic!を引き起こすとテストする
#[test]属性の後、適用するテスト関数の前に#[should_panic]属性を配置しています。
このテストが通るときの結果を見ましょう:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
よさそうですね!では、値が100より大きいときにnew関数がパニックするという条件を除去することでコードにバグを導入しましょう:
pub struct Guess {
value: i32,
}
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
//予想値は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);
}
}
リスト11-8のテストを実行すると、失敗するでしょう:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
この場合、それほど役に立つメッセージは得られませんが、テスト関数に目を向ければ、
#[should_panic]で注釈されていることがわかります。得られた失敗は、
テスト関数のコードがパニックを引き起こさなかったことを意味するのです。
should_panicを使用するテストは不正確なこともあります。
should_panicのテストは、想定していたもの以外の理由でテストがパニックしても通ってしまうのです。
should_panicのテストの正確を期すために、should_panic属性にexpected引数を追加することもできます。
このテストハーネスは、失敗メッセージに与えられたテキストが含まれていることを確かめてくれます。
例えば、リスト11-9の修正されたGuessのコードを考えてください。ここでは、
new関数は、値が大きすぎるか小さすぎるかによって異なるメッセージでパニックします。
ファイル名: src/lib.rs
pub struct Guess {
value: i32,
}
// --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 = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
リスト11-9: 指定された部分文字列を含むパニックメッセージでpanic!することをテストする
should_panic属性のexpected引数に置いた値がGuess::new関数がパニックしたメッセージの一部になっているので、
このテストは通ります。予想されるパニックメッセージ全体を指定することもでき、今回の場合、
Guess value must be less than or equal to 100, got 200.となります。
何を指定するかは、パニックメッセージのどこが固有でどこが動的か、
またテストをどの程度正確に行いたいかによります。今回の場合、パニックメッセージの一部でも、テスト関数内のコードが、
else if value > 100の場合を実行していると確認するのに事足りるのです。
expectedメッセージありのshould_panicテストが失敗すると何が起きるのが確かめるために、
if value < 1とelse if value > 100ブロックの本体を入れ替えることで再度コードにバグを仕込みましょう:
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
//予想値は100以下でなければなりませんが、{}でした。
"Guess value must be less than or equal to 100, got {}.",
value
);
} else if value > 100 {
panic!(
//予想値は1以上でなければなりませんが、{}でした。
"Guess value must be greater than or equal to 1, got {}.",
value
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
should_panicテストを実行すると、今回は失敗するでしょう:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at src/lib.rs:13:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: `"Guess value must be greater than or equal to 1, got 200."`,
expected substring: `"less than or equal to 100"`
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
この失敗メッセージは、このテストが確かに予想通りパニックしたことを示していますが、
パニックメッセージは、予想される文字列の'Guess value must be less than or equal to 100'を含んでいませんでした。
実際に得られたパニックメッセージは今回の場合、Guess value must be greater than or equal to 1, got 200.でした。
そうしてバグの所在地を割り出し始めることができるわけです!
Result<T, E>をテストで使う
これまで書いてきたテストは失敗するとパニックしていましたが、
Result<T, E>を使うようなテストを書くこともできます!
以下は、リスト11-1のテストを、Result<T, E>を使い、パニックする代わりに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"))
}
}
}
it_works関数の戻り値の型はResult<(), String>になりました。
関数内でassert_eq!マクロを呼び出す代わりに、テストが成功すればOk(())を、失敗すればErrにStringを入れて返すようにします。
Result<T, E> を返すようなテストを書くと、?演算子をテストの中で使えるようになります。
これは、テスト内で何らかの工程がErrヴァリアントを返したときに失敗するべきテストを書くのに便利です。
Result<T, E>を使うテストに#[should_panic]注釈を使うことはできません。
操作がErr列挙子を返すことをアサーションするためには、Result<T, E>値に対して?演算子を使用しないでください。
代わりに、assert!(value.is_err())を使用してください。
今やテスト記法を複数知ったので、テストを走らせる際に起きていることに目を向け、
cargo testで使用できるいろんなオプションを探究しましょう。