テストの書き方

RustRustBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

テストの書き方へようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。

この実験では、属性、マクロ、アサーションを使ってRustでテストを書く方法を学びます。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/ErrorHandlingandDebuggingGroup(["Error Handling and Debugging"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/ErrorHandlingandDebuggingGroup -.-> rust/panic_usage("panic! Usage") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") subgraph Lab Skills rust/variable_declarations -.-> lab-100415{{"テストの書き方"}} rust/integer_types -.-> lab-100415{{"テストの書き方"}} rust/string_type -.-> lab-100415{{"テストの書き方"}} rust/function_syntax -.-> lab-100415{{"テストの書き方"}} rust/expressions_statements -.-> lab-100415{{"テストの書き方"}} rust/method_syntax -.-> lab-100415{{"テストの書き方"}} rust/panic_usage -.-> lab-100415{{"テストの書き方"}} rust/traits -.-> lab-100415{{"テストの書き方"}} end

テストの書き方

テストは、非テストコードが期待通りに機能していることを検証するRust関数です。テスト関数の本体は通常、これらの3つのアクションを実行します。

  • 必要なデータや状態を設定する。
  • テストしたいコードを実行する。
  • 結果が期待通りであることをアサートする。

これらのアクションを行うためのテストを書くためにRustが提供する機能を見てみましょう。これには、test属性、いくつかのマクロ、およびshould_panic属性が含まれます。

テスト関数の構造

最も単純な形で、Rustのテストはtest属性で注釈付けされた関数です。属性は、Rustコードの一部に関するメタデータです。例として、第5章で構造体に使用したderive属性があります。関数をテスト関数に変更するには、fnの前の行に#[test]を追加します。cargo testコマンドでテストを実行すると、Rustは注釈付けされた関数を実行し、各テスト関数が合格するか失敗するかを報告するテストランナーバイナリをビルドします。

Cargoで新しいライブラリプロジェクトを作成するたびに、その中にテスト関数を持つテストモジュールが自動的に生成されます。このモジュールは、テストを書くためのテンプレートを提供します。これにより、新しいプロジェクトを始めるたびに正確な構造と構文を調べる必要がなくなります。必要なだけ多くの追加のテスト関数とテストモジュールを追加できます!

実際にコードをテストする前に、テンプレートテストを使ってテストがどのように機能するかのいくつかの側面を調べます。その後、書いたコードを呼び出し、その動作が正しいことをアサートする、現実世界のテストを書きます。

2つの数を加算する新しいライブラリプロジェクトadderを作成しましょう。

$ cargo new adder --lib
Created library $(adder) project
$ cd adder

adderライブラリのsrc/lib.rsファイルの内容は、リスト11-1のようになるはずです。

ファイル名: src/lib.rs

#[cfg(test)]
mod tests {
  1 #[test]
    fn it_works() {
        let result = 2 + 2;
      2 assert_eq!(result, 4);
    }
}

リスト11-1: cargo newによって自動生成されるテストモジュールと関数

今は、最初の2行を無視して、関数に焦点を当てましょう。#[test]の注釈[1]に注目してください。この属性は、これがテスト関数であることを示しており、テストランナーはこの関数をテストとして扱うようになっています。testsモジュールには、共通のシナリオを設定したり、共通の操作を実行したりするための非テスト関数もあるかもしれません。だから、いつもどの関数がテストであるかを示す必要があります。

例の関数本体は、assert_eq!マクロ[2]を使って、2と2を加算した結果を含むresultが4に等しいことをアサートしています。このアサーションは、典型的なテストの形式の例となっています。これを実行して、このテストが合格することを確認しましょう。

cargo testコマンドは、プロジェクト内のすべてのテストを実行します。リスト11-2に示すようになります。

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.57s
     Running unittests src/lib.rs (target/debug/deps/adder-
92948b65e88960b4)

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

3 test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

  4 Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

リスト11-2: 自動生成されたテストを実行した結果

Cargoはテストをコンパイルして実行しました。running 1 testという行が表示されます[1]。次の行は、生成されたテスト関数の名前であるit_worksと、そのテストの実行結果がokであることが表示されます[2]。全体の要約であるtest result: ok.[3]は、すべてのテストが合格したことを意味しており、1 passed; 0 failedと表示される部分は、合格または失敗したテストの数を合算したものです。

特定のインスタンスでテストを実行しないように、テストを無視することができます。これについては、「特定の要求がない限り一部のテストを無視する」で説明します。ここではそのようなことをしていないので、要約には0 ignoredが表示されます。また、cargo testコマンドに引数を渡して、名前が特定の文字列を含むテストのみを実行することもできます。これを「名前で一部のテストを実行する」で説明します。ここでは、実行するテストをフィルタリングしていないので、要約の末尾には0 filtered outが表示されます。

0 measuredの統計値は、パフォーマンスを測定するベンチマークテスト用のものです。この記事執筆時点では、ベンチマークテストはnightly Rustでのみ利用可能です。詳細は、https://doc.rust-lang.org/unstable-book/library-features/test.htmlのベンチマークテストに関するドキュメントを参照してください。

テスト出力の次の部分は、Doc-tests adder[4]から始まり、ドキュメントテストの結果に関するものです。まだドキュメントテストはありませんが、RustはAPIドキュメントに表示されるコード例をコンパイルすることができます。この機能により、ドキュメントとコードを同期させることができます!「ドキュメントコメントをテストとして」で、ドキュメントテストの書き方について説明します。今は、Doc-testsの出力を無視します。

自分自身のニーズに合わせてテストをカスタマイズし始めましょう。まず、it_works関数の名前をexplorationなどの別の名前に変更します。

ファイル名: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}

そして、もう一度cargo testを実行します。出力には今はit_worksの代わりにexplorationが表示されます。

running 1 test
test tests::exploration... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

今度は、もう1つのテストを追加しますが、今回は失敗するテストを作成します!テスト関数内の何かがパニックすると、テストは失敗します。各テストは新しいスレッドで実行され、メインスレッドがテストスレッドが終了したことを検知すると、テストは失敗としてマークされます。第9章では、パニックする最も簡単な方法はpanic!マクロを呼び出すことであると説明しました。新しいテストをanotherという名前の関数として入力します。すると、src/lib.rsファイルはリスト11-3のようになります。

ファイル名: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
}

リスト11-3: panic!マクロを呼び出すことで失敗する2番目のテストを追加

cargo testを使ってもう一度テストを実行します。出力はリスト11-4のようになり、explorationテストが合格し、anotherテストが失敗したことが示されます。

running 2 tests
test tests::exploration... ok
1 test tests::another... FAILED

2 failures:

---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

3 failures:
    tests::another

4 test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

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

リスト11-4: 1つのテストが合格し、1つのテストが失敗した場合のテスト結果

test tests::anotherの行には、okの代わりにFAILEDが表示されます[1]。個々の結果と要約の間に、2つの新しいセクションが表示されます。最初のセクション[2]は、各テストの失敗の詳細な理由を表示します。この場合、src/lib.rsファイルの10行目でanother'Make this test fail'でパニックしたために失敗したことがわかります。次のセクション[3]は、すべての失敗したテストの名前のみをリストしており、たくさんのテストとたくさんの詳細な失敗したテストの出力がある場合に便利です。失敗したテストの名前を使って、そのテストのみを実行して、より簡単にデバッグすることができます。「テストの実行方法を制御する」で、テストを実行する方法についてもっと詳しく説明します。

要約行は最後に表示されます[4]。全体的なテスト結果はFAILEDです。1つのテストが合格し、1つのテストが失敗しました。

これで、さまざまなシナリオでのテスト結果を見たので、panic!以外の、テストで役立ついくつかのマクロを見てみましょう。

assert! マクロを使った結果のチェック

標準ライブラリによって提供される assert! マクロは、テスト内のある条件が true であることを確認したい場合に便利です。assert! マクロには、ブール値に評価される引数を渡します。値が true の場合、何も起こらず、テストは合格します。値が false の場合、assert! マクロは panic! を呼び出してテストを失敗させます。assert! マクロを使うことで、コードが意図通りに機能していることを確認できます。

リスト5-15では、Rectangle 構造体と can_hold メソッドを使用しました。これらは、リスト11-5に再掲してあります。このコードを src/lib.rs ファイルに入れて、assert! マクロを使ってそれに対するいくつかのテストを書きましょう。

ファイル名: src/lib.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

リスト11-5: 第5章の Rectangle 構造体とその can_hold メソッドの使用

can_hold メソッドはブール値を返すので、assert! マクロに最適なケースです。リスト11-6では、幅が8で高さが7の Rectangle インスタンスを作成し、幅が5で高さが1の別の Rectangle インスタンスを保持できることをアサートすることで、can_hold メソッドをテストします。

ファイル名: src/lib.rs

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

    #[test]
  2 fn larger_can_hold_smaller() {
      3 let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

      4 assert!(larger.can_hold(&smaller));
    }
}

リスト11-6: 大きな四角形が小さな四角形を実際に保持できるかどうかをチェックする can_hold のテスト

tests モジュールの中に新しい行 use super::*; を追加しています[1]。tests モジュールは、「モジュールツリー内のアイテムを参照するためのパス」で説明した通常の可視性ルールに従う通常のモジュールです。tests モジュールは内部モジュールなので、テスト対象のコードを外部モジュールから内部モジュールのスコープに持ち込む必要があります。ここではグロブを使っているので、外部モジュールで定義したものはすべてこの tests モジュールで利用できます。

テストの名前を larger_can_hold_smaller にしています[2]。そして、必要な2つの Rectangle インスタンスを作成しています[3]。その後、assert! マクロを呼び出し、larger.can_hold(&smaller) の呼び出し結果を渡しています[4]。この式は true を返すはずなので、テストは合格するはずです。確認してみましょう!

running 1 test
test tests::larger_can_hold_smaller... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

合格しました!もう1つのテストを追加しましょう。今度は、小さな四角形が大きな四角形を保持できないことをアサートします。

ファイル名: src/lib.rs

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

    #[test]
    fn larger_can_hold_smaller() {
        --snip--
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

この場合、can_hold 関数の正しい結果は false なので、assert! マクロに渡す前にその結果を否定する必要があります。その結果、can_holdfalse を返す場合、テストは合格します。

running 2 tests
test tests::larger_can_hold_smaller... ok
test tests::smaller_cannot_hold_larger... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

2つのテストが合格しました!では、コードにバグを入れた場合、テスト結果がどうなるか見てみましょう。can_hold メソッドの実装を変更して、幅を比較する際の大なり記号を小なり記号に置き換えます。

--snip--

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width < other.width && self.height > other.height
    }
}

今、テストを実行すると次のような結果になります。

running 2 tests
test tests::smaller_cannot_hold_larger... ok
test tests::larger_can_hold_smaller... FAILED

failures:

---- tests::larger_can_hold_smaller stdout ----
thread'main' panicked at 'assertion failed:
larger.can_hold(&smaller)', src/lib.rs:28:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace


failures:
    tests::larger_can_hold_smaller

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

テストがバグを検出しました!larger.width が8で smaller.width が5なので、can_hold での幅の比較は今では false を返します。8は5未満ではありません。

assert_eq!assert_ne! マクロを使った等価性のテスト

機能を検証する一般的な方法は、テスト対象のコードの結果と、コードが返すはずの値との等価性をテストすることです。これは、assert! マクロを使って == 演算子を使った式を渡すことで行うことができます。しかし、このような一般的なテストのために、標準ライブラリには2つのマクロ assert_eq!assert_ne! が用意されており、これらを使うことでもっと便利にこのテストを行うことができます。これらのマクロは、それぞれ2つの引数を等価性または非等価性で比較します。また、アサーションが失敗した場合、2つの値を表示します。これにより、テストが失敗した理由がわかりやすくなります。逆に、assert! マクロは、== 式に対して false の値を取得したことを示すだけで、false の値に至る値を表示しません。

リスト11-7では、add_two という名前の関数を書きます。この関数は、引数に2を加えます。そして、assert_eq! マクロを使ってこの関数をテストします。

ファイル名: src/lib.rs

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));
    }
}

リスト11-7: assert_eq! マクロを使った add_two 関数のテスト

合格することを確認しましょう!

running 1 test
test tests::it_adds_two... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

assert_eq! に引数として 4 を渡しています。これは、add_two(2) の呼び出し結果と等しいです。このテストの行は test tests::it_adds_two... ok で、ok のテキストが表示されることで、テストが合格したことがわかります!

コードにバグを入れて、assert_eq! が失敗したときの様子を見てみましょう。add_two 関数の実装を変更して、代わりに3を加えるようにします。

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

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

running 1 test
test tests::it_adds_two... FAILED

failures:

---- tests::it_adds_two stdout ----
1 thread'main' panicked at 'assertion failed: `(left == right)`
2   left: `4`,
3  right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

failures:
    tests::it_adds_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

テストがバグを検出しました!it_adds_two テストが失敗し、メッセージには、失敗したアサーションが assertion failed:(left == right)`\[1\] であり、left\[2\] と right\[3\] の値が何であるかが示されています。このメッセージはデバッグを始めるのに役立ちます。left引数は4 でしたが、right引数であるadd_two(2)の結果は5` でした。たくさんのテストを行っている場合、これが特に役立つことが想像できます。

いくつかの言語やテストフレームワークでは、等価性アサーション関数のパラメータは expectedactual と呼ばれ、引数を指定する順序が重要です。しかし、Rustでは、それらは leftright と呼ばれ、期待する値とコードが生成する値を指定する順序は重要ではありません。このテストのアサーションを assert_eq!(add_two(2), 4) のように書くこともできます。これにより、assertion failed:(left == right)` と表示される同じ失敗メッセージが表示されます。

assert_ne! マクロは、与えられた2つの値が等しくなければ合格し、等しければ失敗します。このマクロは、値が何になるかはわからないが、何になるべきではないことは確かである場合に最も役立ちます。たとえば、入力を必ず何らかの方法で変更する関数をテストしている場合、入力がどのように変更されるかはテストを実行する曜日に依存する場合、関数の出力が入力と等しくないことをアサートするのが最善策かもしれません。

表面下では、assert_eq!assert_ne! マクロはそれぞれ ==!= 演算子を使用します。アサーションが失敗した場合、これらのマクロはデバッグフォーマットを使って引数を表示します。これは、比較される値が PartialEqDebug トレイトを実装していることを意味します。すべてのプリミティブ型とほとんどの標準ライブラリ型はこれらのトレイトを実装しています。自分で定義する構造体や列挙型の場合、それらの型の等価性をアサートするには PartialEq を実装する必要があります。また、アサーションが失敗したときに値を表示するには Debug を実装する必要があります。両方のトレイトは派生可能なトレイトであるため、リスト5-12で述べたように、これは通常、構造体や列挙型の定義に #[derive(PartialEq, Debug)] 注釈を追加するだけで簡単に行えます。これらの派生可能なトレイトやその他のトレイトに関する詳細については、付録Cを参照してください。

カスタムの失敗メッセージの追加

assert!assert_eq!、および assert_ne! マクロには、失敗メッセージとともに表示するカスタムメッセージを追加することができます。これは、任意の引数として指定します。必要な引数の後に指定された任意の引数は、format! マクロ(「+演算子またはformat!マクロを使った連結」で説明)に渡されます。これにより、{} プレースホルダとそれに入れる値を含む書式文字列が渡せます。カスタムメッセージは、アサーションの意味を文書化するのに役立ちます。テストが失敗したときに、コードの問題が何であるかをよりよく把握することができます。

たとえば、名前で人を迎える関数があり、関数に渡した名前が出力に表示されることをテストしたいとしましょう。

ファイル名: src/lib.rs

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"));
    }
}

このプログラムの要件はまだ合意されておらず、挨拶の冒頭の Hello のテキストが変更されると思われます。要件が変更されたときにテストを更新する必要がないように、greeting 関数から返される値と厳密に等しいことをチェックする代わりに、出力に入力パラメータのテキストが含まれていることをアサートすることにしました。

では、このコードにバグを入れて、greeting を変更して name を除外して、デフォルトのテスト失敗がどのように見えるか見てみましょう。

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

このテストを実行すると、次のような結果になります。

running 1 test
test tests::greeting_contains_name... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread'main' panicked at 'assertion failed:
result.contains(\"Carol\")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace


failures:
    tests::greeting_contains_name

この結果は、アサーションが失敗したことと、アサーションがある行を示しています。より役立つ失敗メッセージは、greeting 関数からの値を表示するでしょう。greeting 関数から得た実際の値で埋められたプレースホルダを持つ書式文字列で構成されるカスタムの失敗メッセージを追加しましょう。

#[test]
fn greeting_contains_name() {
    let result = greeting("Carol");
    assert!(
        result.contains("Carol"),
        "Greeting did not contain name, value was `{result}`"
    );
}

今、テストを実行すると、より情報が豊富なエラーメッセージが表示されます。

---- tests::greeting_contains_name stdout ----
thread'main' panicked at 'Greeting did not contain name, value
was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

テスト出力に実際に得た値が表示されるので、期待したことではなく何が起こったかをデバッグするのに役立ちます。

should_panic を使ったパニックのチェック

戻り値をチェックするだけでなく、コードが期待通りにエラー条件を処理することをチェックすることも重要です。たとえば、リスト9-13で作成した Guess 型を考えてみましょう。Guess を使用する他のコードは、Guess インスタンスが1から100の間の値のみを含むことを保証に依存しています。そこで、その範囲外の値で Guess インスタンスを作成しようとするとパニックすることを確認するテストを書くことができます。

これは、テスト関数に should_panic 属性を追加することで行います。関数内のコードがパニックする場合、テストは合格します。関数内のコードがパニックしない場合、テストは失敗します。

リスト11-8は、Guess::new のエラー条件が期待通りに発生することを確認するテストを示しています。

// src/lib.rs
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> 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);
    }
}

リスト11-8: 条件がパニックを引き起こすことをテストする

#[should_panic] 属性を、#[test] 属性の後で、それが適用されるテスト関数の前に配置します。このテストが合格したときの結果を見てみましょう。

running 1 test
test tests::greater_than_100 - should panic... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

うまくいっているようです!では、コードにバグを入れて、new 関数が値が100を超えた場合にパニックする条件を削除してみましょう。

// src/lib.rs
--snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be between 1 and 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

リスト11-8のテストを実行すると、失敗します。

running 1 test
test tests::greater_than_100 - should panic... FAILED

failures:

---- tests::greater_than_100 stdout ----
note: test did not panic as expected

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s

この場合、あまり役立たないメッセージが表示されますが、テスト関数を見ると、#[should_panic] で注釈付けされていることがわかります。得られた失敗は、テスト関数内のコードがパニックを引き起こさなかったことを意味します。

should_panic を使用するテストは不正確である可能性があります。should_panic テストは、期待した理由とは別の理由でテストがパニックした場合でも合格します。should_panic テストをより正確にするには、should_panic 属性にオプションの expected パラメータを追加することができます。テストハーネスは、失敗メッセージに提供されたテキストが含まれていることを確認します。たとえば、リスト11-9の Guess の修正コードを考えてみましょう。ここでは、new 関数は値が小さすぎるか大きすぎるかに応じて、異なるメッセージでパニックします。

// src/lib.rs
--snip--

impl Guess {
    pub fn new(value: i32) -> 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 = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

リスト11-9: 指定された部分文字列を含むパニックメッセージでの panic! のテスト

このテストは合格します。なぜなら、should_panic 属性の expected パラメータに入れた値は、Guess::new 関数がパニックするメッセージの部分文字列だからです。この場合、期待する完全なパニックメッセージは Guess value must be less than or equal to 100, got 200 である可能性があります。どのようなパニックメッセージを指定するかは、パニックメッセージのどれが一意または動的であり、テストをどの程度正確にしたいかに依存します。この場合、パニックメッセージの部分文字列だけでも、テスト関数内のコードが else if value > 100 のケースを実行することを保証するのに十分です。

expected メッセージ付きの should_panic テストが失敗したときに何が起こるかを見るために、コードにバグを入れて、if value < 1else if value > 100 のブロックの本体を入れ替えましょう。

// src/lib.rs
--snip--
if value < 1 {
    panic!(
        "Guess value must be less than or equal to 100, got {}.",
        value
    );
} else if value > 100 {
    panic!(
        "Guess value must be greater than or equal to 1, got {}.",
        value
    );
}
--snip--

今度は、should_panic テストを実行すると、失敗します。

running 1 test
test tests::greater_than_100 - should panic... FAILED

failures:

---- tests::greater_than_100 stdout ----
thread'main' panicked at 'Guess value must be greater than or equal to 1, got
200.', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
      panic message: `"Guess value must be greater than or equal to 1, got
200."`,
 expected substring: `"less than or equal to 100"`

failures:
    tests::greater_than_100

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;
finished in 0.00s

失敗メッセージは、このテストが確かに期待通りにパニックしたことを示していますが、パニックメッセージには期待される文字列 'Guess value must be less than or equal to 100' が含まれていません。この場合に得られたパニックメッセージは Guess value must be greater than or equal to 1, got 200 でした。これで、バグがどこにあるかを見つけ始めることができます!

テストでの Result<T, E> の使用

これまでのテストは、失敗するとすべてパニックします。また、Result<T, E> を使ったテストも書くことができます!ここに、リスト11-1のテストを書き直して、Result<T, E> を使ってパニックする代わりに Err を返すようにしたものです。

ファイル名: src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

it_works 関数の返り値の型が今は Result<(), String> になっています。関数の本体では、assert_eq! マクロを呼び出す代わりに、テストが合格したときに Ok(()) を返し、テストが失敗したときに String を含む Err を返します。

テストを書いて Result<T, E> を返すようにすることで、テストの本体で疑問符演算子を使うことができます。これは、その中のどの操作が Err 変数を返した場合にテストが失敗するようなテストを書く便利な方法です。

Result<T, E> を使ったテストには、#[should_panic] 注釈を使うことはできません。操作が Err 変数を返すことをアサートするには、Result<T, E> 値に疑問符演算子を使わないでください。代わりに、assert!(value.is_err()) を使ってください。

これで、いくつかのテストの書き方を知ったので、テストを実行したときに何が起こっているか見て、cargo test で使えるさまざまなオプションを探ってみましょう。

まとめ

おめでとうございます!「テストの書き方」の実験を終えました。さらに実験を通じて技術力を向上させるために、LabExでさまざまな実験を行ってみましょう。