2017年7月22日土曜日

The Rust Programming Language 2nd 15日目 テスト1

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::anotherFAILEDだったと言っている.成功した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によって出力する.ゆえに,比較される値はPartialE1Debug 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_panicexpectedというパラメータを渡して,より厳密なテストを行うことが出来る.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 < 1else 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の様々なオプションを見ていくことにする.

0 件のコメント:

コメントを投稿