テスト

プログラムのテストはバグの存在を示すためには非常に効率的な方法ですが、バグの不存在を示すためには絶望的に不十分です。 エドガー・W・ダイクストラ、『謙虚なプログラマ』(1972)

Rustのコードをテストする方法について話しましょう。 ここではRustのコードをテストする正しい方法について議論するつもりはありません。 テストを書くための正しい方法、誤った方法に関する流派はたくさんあります。 それらの方法は全て、同じ基本的なツールを使うので、それらのツールを使うための文法をお見せしましょう。

test アトリビュート

Rustでの一番簡単なテストは、 test アトリビュートの付いた関数です。 adder という名前の新しいプロジェクトをCargoで作りましょう。

$ cargo new adder
$ cd adder

新しいプロジェクトを作ると、Cargoは自動的に簡単なテストを生成します。 これが src/lib.rs の内容です。

fn main() { #[test] fn it_works() { } }
#[test]
fn it_works() {
}

#[test] に注意しましょう。 このアトリビュートは、この関数がテスト関数であるということを示します。 今のところ、その関数には本文がありません。 成功させるためにはそれで十分なのです! テストは cargo test で実行することができます。

$ cargo test
   Compiling adder v0.0.1 (file:///home/you/projects/adder)
     Running target/adder-91b3e234d4ed382a

running 1 test
test it_works ... ok

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

   Doc-tests adder

running 0 tests

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

Cargoはテストをコンパイルし、実行しました。 ここでは2種類の結果が出力されています。1つは書かれたテストについてのもの、もう1つはドキュメンテーションテストについてのものです。 それらについては後で話しましょう。 とりあえず、この行を見ましょう。

test it_works ... ok

it_works に注意しましょう。 これは関数の名前に由来しています。

fn main() { fn it_works() { } }
fn it_works() {

次のようなサマリも出力されています。

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

なぜ何も書いていないテストがこのように成功するのでしょうか。 panic! しないテストは全て成功で、 panic! するテストは全て失敗なのです。 テストを失敗させましょう。

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

assert! はRustが提供するマクロで、1つの引数を取ります。引数が true であれば何も起きません。 引数が false であれば panic! します。 テストをもう一度実行しましょう。

$ cargo test
   Compiling adder v0.0.1 (file:///home/you/projects/adder)
     Running target/adder-91b3e234d4ed382a

running 1 test
test it_works ... FAILED

failures:

---- it_works stdout ----
        thread 'it_works' panicked at 'assertion failed: false', /home/steve/tmp/adder/src/lib.rs:3



failures:
    it_works

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

thread '<main>' panicked at 'Some tests failed', /home/steve/src/rust/src/libtest/lib.rs:247

Rustは次のとおりテストが失敗したことを示しています。

test it_works ... FAILED

そして、それはサマリにも反映されます。

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

ステータスコードも非0になっています。 OS XやLinuxでは $? を使うことができます。

$ echo $?
101

Windowsでは、 cmd を使っていればこうです。

> echo %ERRORLEVEL%

そして、PowerShellを使っていればこうです。

> echo $LASTEXITCODE # the code itself
> echo $? # a boolean, fail or succeed

これは cargo test を他のツールと統合したいときに便利です。

もう1つのアトリビュート、 should_panic を使ってテストの失敗を反転させることができます。

fn main() { #[test] #[should_panic] fn it_works() { assert!(false); } }
#[test]
#[should_panic]
fn it_works() {
    assert!(false);
}

今度は、このテストが panic! すれば成功で、完走すれば失敗です。 試しましょう。

$ cargo test
   Compiling adder v0.0.1 (file:///home/you/projects/adder)
     Running target/adder-91b3e234d4ed382a

running 1 test
test it_works ... ok

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

   Doc-tests adder

running 0 tests

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

Rustはもう1つのマクロ、 assert_eq! を提供しています。これは2つの引数の等価性を調べます。

fn main() { #[test] #[should_panic] fn it_works() { assert_eq!("Hello", "world"); } }
#[test]
#[should_panic]
fn it_works() {
    assert_eq!("Hello", "world");
}

このテストは成功でしょうか、失敗でしょうか。 should_panic アトリビュートがあるので、これは成功です。

$ cargo test
   Compiling adder v0.0.1 (file:///home/you/projects/adder)
     Running target/adder-91b3e234d4ed382a

running 1 test
test it_works ... ok

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

   Doc-tests adder

running 0 tests

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

should_panic を使ったテストは脆いテストです。なぜなら、テストが予想外の理由で失敗したのではないということを保証することが難しいからです。 これを何とかするために、 should_panic アトリビュートにはオプションで expected パラメータを付けることができます。 テストハーネスが、失敗したときのメッセージに与えられたテキストが含まれていることを確かめてくれます。 前述の例のもっと安全なバージョンはこうなります。

fn main() { #[test] #[should_panic(expected = "assertion failed")] fn it_works() { assert_eq!("Hello", "world"); } }
#[test]
#[should_panic(expected = "assertion failed")]
fn it_works() {
    assert_eq!("Hello", "world");
}

基本はそれだけです! 「リアルな」テストを書いてみましょう。

fn main() { pub fn add_two(a: i32) -> i32 { a + 2 } #[test] fn it_works() { assert_eq!(4, add_two(2)); } }
pub fn add_two(a: i32) -> i32 {
    a + 2
}

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

これは非常に一般的な assert_eq! の使い方です。いくつかの関数に結果の分かっている引数を渡して呼び出し、期待した結果と比較します。

ignore アトリビュート

ときどき、特定のテストの実行に非常に時間が掛かることがあります。 そのようなテストは、 ignore アトリビュートを使ってデフォルトでは無効にすることができます。

fn main() { #[test] fn it_works() { assert_eq!(4, add_two(2)); } #[test] #[ignore] fn expensive_test() { // code that takes an hour to run // 実行に1時間掛かるコード } }
#[test]
fn it_works() {
    assert_eq!(4, add_two(2));
}

#[test]
#[ignore]
fn expensive_test() {
    // 実行に1時間掛かるコード
}

テストを実行すると、 it_works が実行されることを確認できますが、今度は expensive_test は実行されません。

$ cargo test
   Compiling adder v0.0.1 (file:///home/you/projects/adder)
     Running target/adder-91b3e234d4ed382a

running 2 tests
test expensive_test ... ignored
test it_works ... ok

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

   Doc-tests adder

running 0 tests

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

無効にされた高コストなテストは cargo test -- --ignored を使って明示的に実行することができます。

$ cargo test -- --ignored
     Running target/adder-91b3e234d4ed382a

running 1 test
test expensive_test ... ok

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

   Doc-tests adder

running 0 tests

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

--ignored アトリビュートはテストバイナリの引数であって、Cargoのものではありません。 コマンドが cargo test -- --ignored となっているのはそういうことです。

tests モジュール

今までの例における手法は、慣用的ではありません。 tests モジュールがないからです。 今までの例の慣用的な書き方はこのようになります。

fn main() { pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::add_two; #[test] fn it_works() { assert_eq!(4, add_two(2)); } } }
pub fn add_two(a: i32) -> i32 {
    a + 2
}

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

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

ここでは、いくつかの変更点があります。 まず、 cfg アトリビュートの付いた mod tests を導入しました。 このモジュールを使うと、全てのテストをグループ化することができます。また、必要であれば、ヘルパ関数を定義し、それをクレートの一部に含まれないようにすることもできます。 cfg アトリビュートによって、テストを実行しようとしているときにだけテストコードがコンパイルされるようになります。 これは、コンパイル時間を節約し、テストが通常のビルドに全く影響しないことを保証してくれます。

2つ目の変更点は、 use 宣言です。 ここは内部モジュールの中なので、テスト関数をスコープの中に持ち込む必要があります。 モジュールが大きい場合、これは面倒かもしれないので、ここがグロブの一般的な使い所です。 src/lib.rs をグロブを使うように変更しましょう。

fn main() { pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { assert_eq!(4, add_two(2)); } } }
pub fn add_two(a: i32) -> i32 {
    a + 2
}

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

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

use 行が変わったことに注意しましょう。 さて、テストを実行します。

$ cargo test
    Updating registry `https://github.com/rust-lang/crates.io-index`
   Compiling adder v0.0.1 (file:///home/you/projects/adder)
     Running target/adder-91b3e234d4ed382a

running 1 test
test tests::it_works ... ok

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

   Doc-tests adder

running 0 tests

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

動きます!

現在の慣習では、 tests モジュールは「ユニット」テストを入れるために使うことになっています。 単一の小さな機能の単位をテストするだけのものは全て、ここに入れる意味があります。 しかし、「結合」テストはどうでしょうか。 結合テストのためには、 tests ディレクトリがあります。

tests ディレクトリ

結合テストを書くために、 tests ディレクトリを作りましょう。そして、その中に次の内容の tests/lib.rs ファイルを置きます。

fn main() { extern crate adder; #[test] fn it_works() { assert_eq!(4, adder::add_two(2)); } }
extern crate adder;

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

これは前のテストと似ていますが、少し違います。 今回は、 extern crate adder を先頭に書いています。 これは、 tests ディレクトリの中のテストが全く別のクレートであるため、ライブラリをインポートしなければならないからです。 これは、なぜ tests が結合テストを書くのに適切な場所なのかという理由でもあります。そこにあるテストは、そのライブラリを他のプログラムと同じようなやり方で使うからです。

テストを実行しましょう。

$ cargo test
   Compiling adder v0.0.1 (file:///home/you/projects/adder)
     Running target/adder-91b3e234d4ed382a

running 1 test
test tests::it_works ... ok

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

     Running target/lib-c18e7d3494509e74

running 1 test
test it_works ... ok

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

   Doc-tests adder

running 0 tests

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

今度は3つのセクションが出力されました。新しいテストが実行され、前に書いたテストも同様に実行されます。

tests ディレクトリについてはそれだけです。 tests モジュールはここでは必要ありません。全てのものがテストのためのものだからです。

最後に、3つ目のセクションを確認しましょう。ドキュメンテーションテストです。

ドキュメンテーションテスト

例の付いたドキュメントほどよいものはありません。 ドキュメントを書いた後にコードが変更された結果、実際に動かなくなった例ほど悪いものはありません。 この状況を終わらせるために、Rustはあなたのドキュメント内の例の自動実行をサポートします( 注意: これはライブラリクレートの中でのみ動作し、バイナリクレートの中では動作しません)。 これが例を付けた具体的な src/lib.rs です。

fn main() { //! The `adder` crate provides functions that add numbers to other numbers. //! `adder`クレートはある数値を数値に加える関数を提供する //! //! # Examples //! //! ``` //! assert_eq!(4, adder::add_two(2)); //! ``` /// This function adds two to its argument. /// この関数は引数に2を加える /// /// # Examples /// /// ``` /// use adder::add_two; /// /// assert_eq!(4, add_two(2)); /// ``` pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { assert_eq!(4, add_two(2)); } } }
//! `adder`クレートはある数値を数値に加える関数を提供する
//!
//! # Examples
//!
//! ```
//! assert_eq!(4, adder::add_two(2));
//! ```

/// この関数は引数に2を加える
///
/// # Examples
///
/// ```
/// use adder::add_two;
///
/// assert_eq!(4, add_two(2));
/// ```
pub fn add_two(a: i32) -> i32 {
    a + 2
}

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

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

モジュールレベルのドキュメントには //! を付け、関数レベルのドキュメントには /// を付けていることに注意しましょう。 RustのドキュメントはMarkdown形式のコメントをサポートしていて、3連バッククオートはコードブロックを表します。 # Examples セクションを含めるのが慣習で、そのとおり、例が後に続きます。

テストをもう一度実行しましょう。

$ cargo test
   Compiling adder v0.0.1 (file:///home/steve/tmp/adder)
     Running target/adder-91b3e234d4ed382a

running 1 test
test tests::it_works ... ok

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

     Running target/lib-c18e7d3494509e74

running 1 test
test it_works ... ok

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

   Doc-tests adder

running 2 tests
test add_two_0 ... ok
test _0 ... ok

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

今回は全ての種類のテストを実行しています! ドキュメンテーションテストの名前に注意しましょう。 _0 はモジュールテストのために生成された名前で、 add_two_0 は関数テストのために生成された名前です。 例を追加するにつれて、それらの名前は add_two_1 というような形で数値が増えていきます。

まだドキュメンテーションテストの書き方の詳細について、全てをカバーしてはいません。 詳しくは ドキュメントの章 を見てください。

最後の注意:バイナリクレート上のテストは実行 できません 。 ファイルの配置についてもっと知りたい場合は クレートとモジュール セクションを見ましょう。