https://doc.rust-lang.org/book/second-edition/
Apache License Version 2.0
Testing
Rustはコードの中にテストを書き込むことを許している.
How to Write Tests
testはRustの関数で,testでないコードが期待した通りに動いているか確かめる.test関数のbodyはふつう, 1.準備, 2.テストしたいコード, 期待する結果の3つを含んでいる.ここではtest
attribute(属性)といくつかのマクロ,そしてshould_panic
attributeを学ぶ.
The Anatomy of a Test Function
attributeはRustコードのメタデータで,chap.5 ですでにderive
を扱った.test
attribute付きの関数がRustのテストコードである.関数をtest関数にするには,#[test]
をfn
の上の行に書く.cargo test
によってテストを実行し,test関数のどれが成功してどれが失敗したかを返す.
testの働きを実験を,自動生成されるtemplate testを通して見ていく.そのあと実際のtestを書いてみる.
ライブラリプロジェクトadder
を生成すると,src/lib.rsにはすでにテストコードが書いてある.
src/lib.rs listing 11-1
#[cfg(test)]
mod tests {
#[test]
fn it_works() {}
}
}
it_works() {}
は何もしないから,テストを無事通過する.
shell
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.22 secs
Running target/debug/deps/adder-ce99bcc2479f4607
running 1 test
test tests::it_works ... ok // testsモジュールのit_worksが正常と言っている
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
Doc-tests adder // documentに対するテスト
running 0 tests // docを書いていないのでテストは行われない
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
失敗するテストを書いてみる.test functionがどこかでpanicするとテストは失敗する.
src/lib.rs listing 11-3
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
}
#[test]
fn anoter() {
panic!("Make this test fail");
}
}
shell listing 11-4
running 2 tests
test tests::exploration ... ok
test tests::another ... FAILED
failures:
---- tests::another stdout ----
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
error: test failed
test tests::another
がFAILED
だったと言っている.成功したit_works
には触れられず,another
の失敗の理由と,失敗したtestの一覧が表示され,最後にtest全体の要約が表示される.testが失敗するのはpanicが生じたときだけではない次節では,panicは起きないが予期した結果と違った計算を行ったときエラーを出すマクロを学ぶ.
Checking Result with the assert!
Macro
assert!
macroはtest functionがfalse
を返したとき場合にpanic!
を呼び,testを失敗させる.
rectangle/src/lib.rs listing 11-5
#[cfg(test)]
mod tests {
use super::*; // tests modの外に有るstructをスコープに入れる
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle { length:8, width: 7};
let smaller = Rectangle { length:5, width: 1};
assert!(larger.can_hold(&smaller)); // assert!(ture)
}
#[test]
fn smaller_can_not_hold_larger() {
let larger = Rectangle {length: 8, width: 7};
let smaller = Rectangle {length: 5, width: 1};
assert!(!smaller.can_hold(&larger)) // assert!(!false)
}
}
#[derive(Debug)]
pub struct Rectangle {
length: u32,
width: u32,
}
impl Rectangle {
pub fn can_hold(&self, other: &Rectangle) -> bool {
self.length > other.length && self.width > other.width
}
}
listing 11-5では,Rectangleのcan_holdメソッドが真となる場合と偽になる場合の両方を確かめている.計算の結果がfalseであることを確かめたいなら,assert!(!false)
によって,確かにfalseである場合のみtestを通過させるようにできる.
shell
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_can_not_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
また,コードにバグを埋め込んでみる.ここではRectangle.can_holdの不等号演算子の一つを逆にしてみる.
self.length < other.length && self.width > other.width
結果は
shell
running 2 tests
test tests::smaller_can_not_hold_larger ... ok
test tests::larger_can_hold_smaller ... FAILED
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:10
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
と,やはり失敗したtestの詳細と全体の要約を出力してくる.
Testing Equality with the assert_eq!
and assert_ne!
Macros
ある関数が適当なfunc
が数値や文字列xxx
を返すときにのみ通過するテストは,assert(func()== xxx)
などとすれば書けるのだが,手間を省くためにassert_eq!(xxx, func())
として同じ意味になるマクロassert_eq!
が定義されている.また,assert_ne!(xxx, funct())
はfunc()
の返り値がxxx
でない場合のみ通過する.どちらのマクロも,テストを通過しなかったときには問題となっている関数の返り値と想定された値を出力する.例えば
src/lib.rs listing 11-7
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));
}
}
は無事通過し,ここでadd_two
のbodyをa+3
に書き換えてテストを再度実行すると
shell
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at 'assertion failed: `(left == right)` (left: `4`, right: `5`)', src/lib.rs:11
note: Run with `RUST_BACKTRACE=1` for a backtrace.
と,左辺,すなわち予期した値は4であるのに,返り値が5であったとしてエラーを返してくる.
ここで我々はassert_eq!(xxx, func())
と,左辺に左に予期した値,右に関数を書いたが,この順序が逆でも構わないし,両方が関数でも構わない.例えば
src/lib.rs listing 11-7-0
pub fn add_two(a: i32) -> i32 {
a + 2
}
pub fn mut_3(a: i32) -> i32 {
a * 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mul_and_add() {
assert_eq!(mut_3(4), add_two(10));
}
}
はテストを通過する.
assert_eq!
とassert_ne!
は内部で==
と!=
演算子をそれぞれ使っており,また失敗時にはマクロの引数をdebug formattingによって出力する.ゆえに,比較される値はPartialE1
とDebug
traitを実装していなければならない.全ての基本型と殆どの標準ライブラリ型はこれらのtraitを実装しているが,プログラマが実装したstructやenumにassert_eq!
やassert_ne!
を適用するには,以上のtraitを実装しなければならない.しかしこれらのtraitはderivableだから,chap.5で見たように,#[derive(PartialEq, Debug)]
定義時に注釈することで,簡単に実装できる.derivable traitについてはappendix Cに詳しい.
Custom Failure Messages
テストが失敗したときに好きなメッセージを出力させることが出来る.assert!
は1つ,assert_eq!
, assert_ne!
は2つの引数を必ず取るが,さらに引数を与えると,それらはformat!
マクロによって加工されるので,format stringと適当な変数を引数に渡すと,適当にパースして出力してくれる.例えば,人名を引数としてその人を歓迎する関数を作ってテストするときには以下のようなコードが考えられる.
src/lib.rs listing
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"));
}
}
これはテストを通過する.greeting
のbodyをString::from("Hello!")
としてバグを入れると,
shell
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'assertion failed: result.contains("Carol")', src/lib.rs:12
note: Run with `RUST_BACKTRACE=1` for a backtrace.
とエラーを生じる.assertionが失敗したことを言っているが,よりエラーを見やすくするために,greeting
関数の返り値を表示するようにする.
src/lib.rs
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`", result
); // 第二引数はプレースホルダー{}を持てる文字列で,
// 第三引数以降がそのプレースホルダーに入る.
}
ここでまたテストを行うと
shell
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'Greeting did not contain name, value was 'Hello'', src/lib.rs:12
note: Run with `RUST_BACKTRACE=1` for a backtrace.
と,確かにエラーメッセージが想定したとおりになる.
Checking for Panics with should_panic
予期した通りの値を返すかを確かめるのと同じくらいに,発生したエラーを予期したとおりに対処するか確かめるのは重要である.たとえばChap. 9, listing9-8で定義したGuess
型で,そのinstanceは必ず1から100の値を取ることを約束したので,Guess
のinstanceでその範囲から外れたものを作ろうとしたときには確かにpanicを起こすことを確かめたい.
これをshould_panic
attributeを関数につけて実現する.shold_panic
は,それがつけられた関数がpanicを起こすときにのみテストを通過するようにする.
src/lib.rs listing 11-8
struct Guess {
value: u32,
}
impl Guess {
pub fn new(value: u32) -> Guess {
if value < 1 || value > 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);
}
}
これは確かにテストを通過する.
shell
running 1 test
test tests::greater_than_100 ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
ここでnew()
における条件を外すと,
shell
running 1 test
test tests::greater_than_100 ... FAILED
failures:
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
と,正常にGuess
の新しいinstanceが作られてしまうので,エラーを生じる.should_panic
は,予期した形のpanicでなくともpanicを拾うとテストに通してしまうので,should_panic
にexpected
というパラメータを渡して,より厳密なテストを行うことが出来る.expected
には文字列が入って,panic時のメッセージにその文字列が現れるときのみテストを通すようにする.例えば
src/lib.rs listing 11-9
struct Guess {
value: u32,
}
impl Guess {
pub fn new(value: u32) -> Guess {
if value < 1 {
panic!("Guess value must be greater than or equal to 1, got {}.", value);
}
else if value > 100 {
panic!("Guess value must be less than or equal to 100, got {}", value);
}
Guess {
value
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
をテストにかけると,確かにpanicが生じ,しかも値が100を上回るときのメッセージが与えられるから,テストを通過する.
また,if value < 1
とelse if value > 100
において数値と不等号を交換すると,
shell
test tests::greater_than_100 ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
note: Panic did not include expected string 'Guess value must be less than or equal to 100'
と,予期したメッセージと返されたメッセージが異なるため,panicが生じてもshould_panic
はテストを通過させない.
以上でテストの書き方を学んだので,つぎはテストを行っているとき内部で何が起きているかとか,cargo test
の様々なオプションを見ていくことにする.