2017年7月23日日曜日

The Rust Programming Language 2nd 16日目 テスト2

https://doc.rust-lang.org/book/second-edition/
Apache License Version 2.0

Testing

Rustはコードの中にテストを書き込むことを許している.

Controlling How Tests are Run

cargo testにオプションを与えてデフォルトとは異なる動作をさせることが出来る.オプションはcargo test --aaa --bbb というふうに書く.cargo testそのものに与えるオプションはaaaに,生成されるバイナリに与えるオプションはbbbに列挙する.

Running Tests in Parallel or Consecutively

デフォルトでは,それぞれのテストはいちいち結果を出力せずに平行実行され,最後に全ての結果を出力する.これは高速だが,以前行ったテストの結果や途中の計算を利用したテストを行えないという問題が有る.たとえばtest-output.txtというファイルを作成し,その中に何かしらのデータを書き込み,そのファイルがテストごとに異なる特定の値を含んでいるという一連のテストを考える.平行実行によるテストでは同じ名前のファイルの作成によって上書きが生じてしまい,正しいコードでもテストに通らなくなるおそれがある.この問題を解決するため,テストを一つずつ実行するという手段が有る.これを実現するには実行ファイルに--test-threadフラグを与える.
$cargo test -- --test-threads=1とすれば,テストを一つずつ実行するようになる.

Showing Function Output

あるテスト関数の中での標準出力は,そのテストが成功するとtest libraryに回収されて,出力されない.例えばprintln!を中に含むテストが成功すると,シェルからそのprintln!が出力した値を見ることはできない.一方テストが失敗すれば全ての標準出力とエラーメッセージがシェルから見れる.
例えばlisting 11-10は成功するテストと失敗するテストを持つ.
src/lib.rs listing 11-10

fn prints_and_returns_10(a: i32) -> i32 {
  println!("I got the value {}", a);
  10
}

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

  #[test]
  fn this_test_will_pass() {
    let value = prints_and_returns_10(4);
    assert_eq!(10, value);
  }

  fn this_test_will_fail() {
    let value = prints_and_returns_10(8);
    assert_eq!(5, value);
  }
}

shell

running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok


failures:

---- tests::this_test_will_fail stdout ----
        I got the value 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)` (left: `5`, right: `10`)', src/lib.rs:19
note: Run with `RUST_BACKTRACE=1` for a backtrace.


failures:
    tests::this_test_will_fail

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured

error: test failed, to rerun pass '--lib'

たしかにI got the value 8と,エラー時のみ標準出力が現れている.通過したテストの標準出力を見たい場合,--nocaptureフラグを使う.
$ cargo test -- --nocaptureによって
shell

running 2 tests
I got the value 4
I got the value 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)` (left: `5`, right: `10`)', src/lib.rs:19

note: Run with `RUST_BACKTRACE=1` for a backtrace.
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED

と,たしかに両方の標準出力がシェルから見れるようになっている.

Running a Subset of Tests by Name

全てのテストを行うと時間がかかりすぎるときは,cargo testの引数としてどのテストを走らせるかを指定することが出来る.
例えば
src/lib.rs listing 11-11

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

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

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

  #[test]
  fn add_three_and_two() {
    assert_eq!(5, add_two(3));
  }

  #[test]
  fn one_hunder() {
    assert_eq!(102, add_two(100));
  }
}

listing 11-11のコードは明らかに全てのテストが通過する.

Running Single Tests

cargo testの引数にテスト関数名を入れることで,そのテスト関数だけを実行できる.

$ cargo test one_hundred
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running target/debug/deps/adder-06a75b4a1f2515e9

running 1 test
test tests::one_hundred ... ok

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

Ignore Some Tests Unless Specifically Requested

非常に時間がかかるテストを,特に指定した場合以外実行したくないときがある.このときは,テスト関数にtest attributeに加えてignore attributeをつける.
src/lib.rs

#[test]
fn it_works() {
  assert!(true);
}

#[test]
#[ignore]
fn expensive test() {
  // code that takes and hour tu run
}

このようにすれば,ただcargo testとしてもexpensive_testは実行されない.ignoreした関数も実行したいときには,cargo test -- --ignoredとオプションをつける.

The Organization

テストには大きく分けてunit(単体)テストとintegration(結合)テストが有る.unit testはそれぞれのモジュールを切り離して,private interfacesについてテストする.integration testはpublic interfaceと既存のモジュールのみを使って,外からコードを利用しているのと同じ方法でテストされる対象にアクセスし,テストする

Unit tests

コードのそれぞれの部分を他と切り離し,その部分が予期したとおり動くかを試す.unit testはsrc directoryに,テストされるコードとともに書く.testsという名前のcfg(test) annotation モジュールを作って中にテスト関数を列挙するのが慣習である.

The Tests Module and #[cfg(test)]

#[cfg(test)] annotationはRustに,cargo testコマンドが実行されたときにのみこのモジュールをコンパイルするように命令する.cargo buildが高速かつ生成される実行ファイルが小さくてすむ.integration testはコードとは別のところに書くので,#[cfg(test)]を書く必要はない.
新しくライブラリプロジェクトを生成すると,src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
    }
}

と,予めテストの雛形が自動的に書き込まれている.cfgとはconfigurationの略であって,以下のコードをtestに設定している.testに設定されたコードはcargo test時のみコンパイルされる.

Testing Private Functions

privateな関数をテストする.
src/lib.rs listing 11-12

pub fn add_two(a: i32) -> i32 {             // public
  internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {  // private
  a + b
}

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

  #[test]
  fn internal() {
    assert_eq!(4, internal_adder(2, 2));
  }
}

Privacy Rules

あるアイテムがpublicであるとき,親moduleを通じてそのアイテムにアクセスできる.
あるアイテムがprivateであるとき,そのアイテムがあるmoduleとその子moduleからしかそのアイテムにはアクセスできない.

を思い出すと,testsadd_twointernal_adderが定義されたスコープの子moduleだから,internalinternal_adderにアクセスできて,テストできる.