Rustのテスト
Rustでは、テストの仕組みが言語と cargo に最初から組み込まれています。そのため、始めるために別のライブラリを探す必要はありません。このページでは、最初のテストを書いて実行するのに必要なことだけを紹介します。
基本的なテストを書く
Section titled “基本的なテストを書く”テストは、#[test] という印を付けたふつうの関数です。
fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn it_adds_two_numbers() { assert_eq!(add(2, 3), 5); }}いくつかポイントを見ておきましょう。
#[cfg(test)]は、「このモジュールはテストを実行するときだけコンパイルする」という意味です。ふだんのプログラムには含まれません。use super::*;は、親モジュールにあるもの(ここではadd関数)をすべて取り込みます。これでテストからaddを呼び出せます。#[test]は、その関数がcargo testで実行すべきテストであることを示します。
テストを実行する
Section titled “テストを実行する”プロジェクトのフォルダで、次のコマンドを実行します。
cargo testすると、次のような出力が表示されます。
running 1 testtest tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignoredassert! とその仲間
Section titled “assert! とその仲間”ふだん必要になることのほとんどは、次の3つのマクロでまかなえます。
#[test]fn checks() { assert!(2 + 2 == 4); // 条件が false ならテストは失敗する assert_eq!(2 + 2, 4); // 2つの値が等しくなければ失敗する assert_ne!(2 + 2, 5); // 2つの値が等しければ失敗する}ふつうは、assert!(a == b) と書くよりも assert_eq! や assert_ne! を使うのがおすすめです。失敗したときに両方の値を表示してくれるので、ただ「assertion failed」と出るだけでなく、何が食い違ったのかがすぐにわかります。
テストが失敗するとどうなるか
Section titled “テストが失敗するとどうなるか”fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn this_will_fail() { assert_eq!(add(2, 2), 5); }}test tests::this_will_fail ... FAILED
assertion `left == right` failed left: 4 right: 5テストが失敗したときに欲しいのは、まさにこういうメッセージです。left がコードが実際に返した値、right がテストで期待した値です。両方が並んで表示されるので、どこがおかしいのかがひと目でわかります。
関数がパニックすることをテストする
Section titled “関数がパニックすることをテストする”正しい動きが「パニックすること」である場合もあります。たとえば、不正な入力を受け付けてはいけない関数です。そんなときは #[should_panic] を使います。
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("0で割ることはできません"); } a / b}
#[cfg(test)]mod tests { use super::*;
#[test] #[should_panic] fn dividing_by_zero_panics() { divide(10, 0); }}失敗メッセージをわかりやすくする
Section titled “失敗メッセージをわかりやすくする”テストが失敗したときは、なぜ失敗したのかをできるだけ早く知りたいものです。ちょっとした2つの工夫が大いに役立ちます。
独自のメッセージ:assert!、assert_eq!、assert_ne! はどれも、値のあとに追加のメッセージを受け取れます。書き方は println! と同じです。このメッセージは、チェックが失敗したときだけ表示されます。
should_panic の expected:ただの #[should_panic] は、どんなパニックでも成功してしまいます。別のバグが原因のパニックでもです。expected = "..." を付けると、パニックのメッセージにその文字列が含まれているときだけ成功するようになります。
もう一度 divide 関数を使いましょう。実行できるように main を付け、両方の工夫を使ったテストを2つ用意しました。
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("0で割ることはできません"); } a / b}
fn main() { println!("10 / 2 = {}", divide(10, 2)); println!("7 / 2 = {}", divide(7, 2));}
#[cfg(test)]mod tests { use super::*;
#[test] fn rounds_down() { let result = divide(7, 2); assert_eq!(result, 3, "7 / 2 は切り捨てになるはずですが、結果は {result} でした"); }
#[test] #[should_panic(expected = "0で割る")] fn dividing_by_zero_panics() { divide(10, 0); }}実行結果:
10 / 2 = 57 / 2 = 3上の実行結果は cargo run で表示されるものです。main はテストを実行しないからです。テストを実行するには cargo test を使います。Rust Playground では、Run の横にあるメニューを開いて Test を選びます。すると、次のように表示されます。
running 2 teststest tests::rounds_down ... oktest tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered outテストは並列に実行されるので、2つの test ... の行はどちらが先に表示されるかわかりません。では、テストが 3 ではなく 4 を期待していたとしましょう。失敗したときの表示には、2つの値のすぐ横にあなたのメッセージが出てきます。
assertion `left == right` failed: 7 / 2 は切り捨てになるはずですが、結果は 3 でした left: 3 right: 4また、divide が別のメッセージでパニックした場合は、expected のチェックがそれを見つけてくれます(ここでは expected = "負の数" だったとします)。
note: panic did not contain expected string panic message: "0で割ることはできません" expected substring: "負の数"これらの英語のメッセージはRust自身が表示するものです。「パニックのメッセージに、期待した文字列が含まれていなかった」という意味です。
なぜテストを書くのか
Section titled “なぜテストを書くのか”正直に言うと、ちょっとしたスクリプトならテストは書かないことが多いでしょう。テストが役に立つのは、そのコードをあとで再利用したり変更したりするときです。何かを壊してしまっても、ユーザーに指摘される前に cargo test がすぐに教えてくれます。だから安心してリファクタリングできるのです。
テストはドキュメントの中に置くこともできます。/// のドキュメントコメントに書いたコード例も cargo test で実行されるので、例が古くなってしまうことがありません。やり方はRustのドキュメントのページで紹介しています。
どちらの問題も、解答には実行できる main と、cargo test(または Playground の Test)で実行するテストが入っています。
1. 成績をつける関数をテストする
Section titled “1. 成績をつける関数をテストする”90点以上なら 'A'、80〜89点なら 'B'、70〜79点なら 'C'、それより低ければ 'F' を返す fn grade(score: u32) -> char を書いてください。そのうえで、90と89のような境界の値を確かめるテストを書きましょう。ヒント:バグが隠れているのは境界です。ちょうど 90 を確かめるテストがあれば、>= と書くべきところを > と書いてしまう、よくある間違いを見つけられます。
解答を見る
fn grade(score: u32) -> char { if score >= 90 { 'A' } else if score >= 80 { 'B' } else if score >= 70 { 'C' } else { 'F' }}
fn main() { for score in [95, 85, 72, 40] { println!("{score} -> {}", grade(score)); }}
#[cfg(test)]mod tests { use super::*;
#[test] fn edges_of_a() { assert_eq!(grade(90), 'A'); assert_eq!(grade(89), 'B'); }
#[test] fn edges_of_c() { assert_eq!(grade(70), 'C'); assert_eq!(grade(69), 'F'); }
#[test] fn zero_is_f() { assert_eq!(grade(0), 'F', "0点は 'F' になるはずです"); }}実行結果:
95 -> A85 -> B72 -> C40 -> Fcargo test を実行すると 3 passed と表示されるはずです。試しに score >= 90 を score > 90 に変えて、もう一度テストを実行してみましょう。edges_of_a が失敗し、left: 'B' と right: 'A' が表示されます。
2. Option を返す関数をテストする
Section titled “2. Option を返す関数をテストする”b が0のときにパニックする代わりに None を返す fn safe_divide(a: i32, b: i32) -> Option<i32> を書いてください。ふつうの割り算のテストと、0で割るときのテストを1つずつ書きましょう。ヒント:assert_eq! は Option にも使えるので、assert_eq!(safe_divide(1, 0), None) と書けます。
解答を見る
fn safe_divide(a: i32, b: i32) -> Option<i32> { if b == 0 { None } else { Some(a / b) }}
fn main() { println!("{:?}", safe_divide(10, 2)); println!("{:?}", safe_divide(1, 0));}
#[cfg(test)]mod tests { use super::*;
#[test] fn divides_normally() { assert_eq!(safe_divide(10, 2), Some(5)); }
#[test] fn zero_gives_none() { assert_eq!(safe_divide(1, 0), None); }}実行結果:
Some(5)None上の divide 関数と比べてみてください。あちらでは0で割るとパニックになるので、テストには #[should_panic] が必要でした。こちらはふつうの戻り値なので、ただの assert_eq! で十分です。Option や Result を返す関数のほうが、たいていテストしやすいのです。
- 関数に
#[test]を付けるとテストになります。すべてのテストはcargo testで実行できます。 - 日常的なチェックのほとんどは
assert!、assert_eq!、assert_ne!でまかなえます。失敗時のメッセージがわかりやすいので、assert_eq!/assert_ne!を優先しましょう。 #[should_panic]を使うと、関数が期待どおりにパニックするかをテストできます。expected = "..."を付ければ、正しい理由でパニックしたかも確かめられます。- どの assert マクロにも独自のメッセージを付けられます。失敗したときに、何がおかしいのかを説明できます。
- テストがいちばん役に立つのは、あとで変更したり再利用したりするコードです。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. cargo test で実行するテストとして関数に付ける属性はどれですか?
2. ふつうの assert!(a == b) よりも assert_eq! や assert_ne! がおすすめなのはなぜですか?
3. #[should_panic] は何をしますか?
スコア: 0 / 3
さらに詳しく
Section titled “さらに詳しく”- テストの記述法 — The Rust Programming Language 日本語版:
#[test]、assert マクロ、独自のメッセージ、should_panic、Resultを返すテストを解説しています。 - テストの実行のされ方を制御する — The Rust Programming Language 日本語版:名前を指定して1つのテストだけ実行する方法、
println!の出力を表示する方法、#[ignore]について。 - テストの体系化 — The Rust Programming Language 日本語版:単体テストと、
tests/フォルダに置く結合テストの違い。 - ユニットテスト — Rust By Example 日本語版:成功するテスト、失敗するテスト、パニックするテストの短い例。
cargo test— The Cargo Book(英語):cargo testコマンドのすべてのオプション。