[Rustのはじめ方] Part28: 単体テストとドキュメントテスト

Rust

コードの品質と信頼性を高めるテスト手法を学ぼう!

はじめに

ソフトウェア開発において、テストは非常に重要なプロセスです。テストを行うことで、コードが期待通りに動作することを確認し、バグを早期に発見して修正することができます。Rustは、コードの品質と信頼性を高めるためのテスト機能を標準で備えています。

このステップでは、Rustにおける主要なテスト方法である単体テストドキュメントテストについて学びます。これらのテストを習慣づけることで、より堅牢なプログラムを作成できるようになります 💪。

単体テスト (Unit Tests)

単体テストは、プログラムの個々の部品(関数やモジュールなど)が正しく機能するかどうかを検証するためのテストです。他の部分から隔離された状態で、特定の機能に焦点を当ててテストを行います。

テスト関数の書き方

Rustでは、関数に #[test] アトリビュートを付けることで、その関数がテスト関数であることを示します。テスト関数は通常、テスト対象のコードと同じファイル、または tests ディレクトリ内の別のファイルに記述されます。慣例として、テスト関数をまとめるための tests という名前のモジュールを作成し、そのモジュールに #[cfg(test)] アトリビュートを付けます。これにより、cargo test を実行したときのみテストコードがコンパイル・実行されるようになります。


// src/lib.rs や src/main.rs など

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

// テストモジュール
#[cfg(test)]
mod tests {
    use super::*; // 親モジュール(この場合はファイル直下)の要素をインポート

    #[test] // この関数がテスト関数であることを示す
    fn it_works() {
        let result = add(2, 2);
        // アサーションマクロで結果を検証
        assert_eq!(result, 4); // resultが4と等しいかチェック
    }

    #[test]
    fn another_test() {
        // ここに別のテストを記述
        assert!(true); // 単純にtrueであることを表明
    }
}

アサーションマクロ

テスト関数内では、コードが期待通りに動作しているかを確認するためにアサーションマクロを使用します。主なマクロには以下のようなものがあります。

  • assert!(expression): expressiontrue であることを表明します。false の場合はパニックします。
  • assert_eq!(left, right): leftright が等しいことを表明します。等しくない場合はパニックし、値の違いを表示します。
  • assert_ne!(left, right): leftright が等しくないことを表明します。等しい場合はパニックします。

これらのマクロは、テストの成否を判断するための基本的なツールです。

パニックを期待するテスト

特定の条件下でコードが意図通りにパニックすることを確認したい場合もあります。そのような場合は、#[test] アトリビュートに should_panic を追加します。


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

    #[test]
    #[should_panic(expected = "Guess value must be between 1 and 100")] // 特定のメッセージでパニックすることを期待
    fn less_than_1() {
        Guess::new(0);
    }
}

should_panicexpected 引数を指定すると、パニックメッセージの一部が期待通りであるかも検証できます。

プライベート関数のテスト

Rustのテストモジュールは、同じファイル内(または同じクレート内)のプライベートな要素にもアクセスできます。そのため、公開されていない内部的な関数も直接テストすることが可能です。これは、内部実装の詳細をテストする際に便利です。


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

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

    #[test]
    fn internal() {
        assert_eq!(internal_adder(2, 2), 4); // プライベート関数をテスト
    }
}

テストの実行

プロジェクトのルートディレクトリで以下のコマンドを実行すると、すべての単体テスト(および後述するドキュメントテスト)が実行されます。


cargo test

テストが成功すると ok、失敗すると FAILED と表示され、失敗したテストの詳細が出力されます。特定のテスト関数だけを実行したい場合は、関数名を引数として渡します。


cargo test it_works # it_works 関数のみ実行
cargo test tests:: # tests モジュール内の全テストを実行

ドキュメントテスト (Documentation Tests) 📚

ドキュメントテストは、コードコメント内に記述されたサンプルコードが正しく動作するかどうかを確認するためのテストです。これにより、ドキュメントの例が常に最新で正確であることが保証されます。Rustでは、ドキュメントコメント(/// または //! で始まるコメント)内のコードブロックがテスト対象となります。

ドキュメントテストの書き方

ドキュメントコメント内に、バッククォート3つ(“`)で囲まれたコードブロックを記述します。言語名として rust を指定するのが一般的ですが、指定しなくてもRustコードとして扱われます。


// src/lib.rs

/// Adds two to the given number.
///
/// # Examples
///
/// ```/// let arg = 5;
/// let answer = my_crate::add_two(arg);
///
/// assert_eq!(7, answer);
/// ```
/// // 上記の ``` ``` 内のコードがドキュメントテストとして実行される
pub fn add_two(x: i32) -> i32 {
    x + 2
}

// もしクレート名が必要なら、実際のクレート名に置き換えるか、
// または以下のように `use` を使って書くこともできます。
/// ```
/// use your_crate_name::add_two; // use 文も書ける
/// assert_eq!(add_two(3), 5);
/// ```

// エラーを期待するドキュメントテスト
/// ```should_panic
/// use your_crate_name::add_two;
/// // パニックする例
/// // add_two(-100); // 例:もし負数を扱わない仕様ならパニックさせるなど
/// ```

// コンパイルエラーになるべきコード(例:型エラーなど)を示す場合
/// ```compile_fail
/// use your_crate_name::add_two;
/// // let result = add_two("hello"); // 文字列は渡せない
/// ```

// デフォルトで表示されないが、テストはされるコード
/// ```rust,ignore
/// // このコードはドキュメントには表示されにくいが、テストは実行される(状況による)
/// // 通常はテスト不要な補助コードなどに使うことがある
/// ```
/// // `ignore` は主にドキュメント生成ツールでの表示制御に使われることが多いが、
/// // `cargo test` では実行されることに注意。テスト実行も無視したい場合は `#` でコメントアウトする。

// コードブロック内で `?` を使う場合(main関数や #[test] 関数内でのみ有効)
/// ```rust
/// fn main() -> Result<(), std::io::Error> { // Result を返す関数内なら `?` が使える
///     // use std::fs::File;
///     // let file = File::open("nonexistent_file.txt")?;
///     Ok(())
/// }
/// ```

#[cfg(test)]
mod tests {
    // 単体テストはここに書く
}

// ダミーのクレート名設定(実際のクレート名に依存しないように)
// もし lib.rs なら、通常はクレート名が Cargo.toml で定義される
// 以下はドキュメントテスト内でクレート名を解決するための例(通常不要)
/**
```
# extern crate your_crate_name; // クレート名を仮定
# fn main() {
# let arg = 5;
# let answer = your_crate_name::add_two(arg);
# assert_eq!(7, answer);
# }
```
*/
pub mod your_crate_name { // ドキュメントテスト用のダミーモジュール
    pub use super::add_two;
}

ポイント:

  • ドキュメントテスト内のコードは、独立した main 関数の中にラップされ、必要な extern crate 文が追加されて実行されます。
  • # で始まる行は、ドキュメントには表示されませんが、テストコードとしてはコンパイル・実行されます。セットアップコードなどを隠すのに便利です。
  • ? 演算子を使いたい場合は、コードブロックが Result を返す関数(例: fn main() -> Result<(), ErrorType>)内にあるかのように書く必要があります。

ドキュメントテストの実行

ドキュメントテストは、単体テストと同様に cargo test コマンドで実行されます。特別なオプションは不要です。


cargo test

テスト結果には、単体テストとドキュメントテストの両方の結果が含まれます。

ドキュメントテストの利点

  • ドキュメントの正確性維持: ドキュメント内のコード例が常に動作することを保証します。
  • 使用例の提示: ライブラリや関数の使い方を示す良い例となります。
  • コードとドキュメントの同期: コードを変更した際に、関連するドキュメントテストが失敗すれば、ドキュメントの更新忘れに気づくことができます。

まとめ ✨

今回は、Rustの単体テストとドキュメントテストについて学びました。

  • 単体テストは、#[test] アトリビュートを使って関数単位でコードの正しさを検証します。assert! 系マクロや #[should_panic] を活用します。
  • ドキュメントテストは、ドキュメントコメント内のコード例(“`rust …

コメント

タイトルとURLをコピーしました