注意: 最新版のドキュメントをご覧ください。この第1版ドキュメントは古くなっており、最新情報が反映されていません。リンク先のドキュメントが現在の Rust の最新のドキュメントです。
プログラムのテストはバグの存在を示すためには非常に効率的な方法ですが、バグの不存在を示すためには絶望的に不十分です。 エドガー・W・ダイクストラ、『謙虚なプログラマ』(1972)
Rustのコードをテストする方法について話しましょう。 ここではRustのコードをテストする正しい方法について議論するつもりはありません。 テストを書くための正しい方法、誤った方法に関する流派はたくさんあります。 それらの方法は全て、同じ基本的なツールを使うので、それらのツールを使うための文法をお見せしましょう。
test
アトリビュートRustでの一番簡単なテストは、 test
アトリビュートの付いた関数です。
adder
という名前の新しいプロジェクトをCargoで作りましょう。
$ cargo new adder
$ cd adder
新しいプロジェクトを作ると、Cargoは自動的に簡単なテストを生成します。
これが src/lib.rs
の内容です。
#[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 it_works() {
次のようなサマリも出力されています。
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
なぜ何も書いていないテストがこのように成功するのでしょうか。
panic!
しないテストは全て成功で、 panic!
するテストは全て失敗なのです。
テストを失敗させましょう。
#[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
を使ってテストの失敗を反転させることができます。
#[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つの引数の等価性を調べます。
#[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
パラメータを付けることができます。
テストハーネスが、失敗したときのメッセージに与えられたテキストが含まれていることを確かめてくれます。
前述の例のもっと安全なバージョンはこうなります。
#[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
アトリビュートを使ってデフォルトでは無効にすることができます。
#[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
モジュールがないからです。
今までの例の慣用的な書き方はこのようになります。
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
をグロブを使うように変更しましょう。
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
ファイルを置きます。
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
です。
//! `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
というような形で数値が増えていきます。
まだドキュメンテーションテストの書き方の詳細について、全てをカバーしてはいません。 詳しくは ドキュメントの章 を見てください。