All Articles

Rust のエラーハンドリングと thiserror に関するメモ書き

今回は Rust のテスト記法に関するメモ書きです。

もくじ

Rust のテストについて

Rust では標準で以下の 3 つのテスト機能を利用できます。

  • ユニットテスト
  • 結合テスト
  • ドキュメントテスト

他にもサードパーティーのクレートを追加することで様々なテスト機能を追加することができるようですが、今回は標準で利用可能なテスト方法のうち、主にユニットテストと結合テストを中心に扱うことにします。

ドキュメントテストは今回は使用しないので割愛します。

ユニットテスト

ユニットテストとは、個々の関数やモジュールを単独で検証するテストです。

Rust の場合は、テスト対象のコードと同じファイル内に #[cfg(test)] モジュールとして配置することで実装できます。

fn internal_helper(x: i32) -> i32 {
    x * 2
}

pub fn public_function(x: i32) -> i32 {
    internal_helper(x) + 1
}

#[cfg(test)]  // cargo test 時のみコンパイルされる
mod tests {
    use super::*;  // 親モジュールの全アイテムをインポート

    #[test]
    fn test_internal_helper() {
        // private 関数にも直接アクセスできる
        assert_eq!(internal_helper(5), 10);
    }

    #[test]
    fn test_public_function() {
        assert_eq!(public_function(5), 11);
    }
}

ユニットテストの基本構造

シンプルなユニットテストの実装のため、以下の関数を実装します。

/// 二つの整数を加算する
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

これに対して、テストモジュールは同じファイルに #[cfg(test)] アノテーションを記述した上で実装します。

参考:Test Organization - The Rust Programming Language

#[cfg(test)] アノテーションが記述されたテストモジュールは cargo test を実行したときにだけ Rust によりコンパイル/実行されます。

ユニットテストはプロダクションのソースコードと同じファイルに記載するため、ビルド時に生成物にテストモジュールが含まれないようにするためにも #[cfg(test)] アノテーションを使用しています。(結合テストの場合は別のファイルに記述するため、#[cfg(test)] アノテーションは不要です。)

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn test_add_basic() {
        let result = add(2, 3);
        assert!(result == 5);
    }

}

このようなテストモジュールについては cargo test もしくは cargo test <テスト関数> でテストを実行できます。

例えば上記の例では、cargo test test_add_basic を実行することで add 関数のテストに成功することを確認できます。

image-20260531161743127

ユニットテストテストのサンプル 1

Rust のユニットテストでは、標準の assert! マクロにより評価を行うことができます。

例えば、以下の 3 つの関数をテスト用に作成します。

/// 二つの整数を加算する
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// 二つの整数を乗算する
pub fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

/// 文字列が空かどうかを判定する
pub fn is_empty(s: &str) -> bool {
    s.is_empty()
}

これらの関数の挙動をテストするため、いくつかのテスト関数をそれぞれ使用できます。

まず以下の関数は、assert! マクロを使用して add(2, 3) が 5 となるかを検証します。

assert! は条件が true であることを検証できます。

#[test]
fn test_add_basic() {
    let result = add(2, 3);
    assert!(result == 5);
}

assert! マクロではテスト失敗時のテキストを定義することもできます。

#[test]
fn test_add_with_text() {
    let result = add(2, 3);
    assert!(result == 5,"2 + 3 = 5 ですが、実際の結果は {} でした。",result);
}

上記のテストに失敗した場合、以下のようなテキストを表示できます。

image-20260531163634374

また、assert_eq! というマクロを使用することもできます。

このマクロでは、パラメータとして与えられた 2 つの要素の値が一致することを確認できます。

#[test]
fn test_add_with_assert_eq() {
    assert_eq!(add(2, 3), 5);
    assert_eq!(add(0, 0), 0);
    assert_eq!(add(-1, 1), 0);
}

逆に、assert_ne! マクロは 2 つの要素が一致しないことを確認することができます。

以下の例の場合には、multiply(3, 4) の実行結果が 12 となるため、2 つめのパラメータが 12 の場合にテストに失敗します。

#[test]
fn test_multiply_not_zero() {
    assert_ne!(multiply(3, 4), 0);
}

また、テスト関数は通常の関数のように使用できるため、以下のように変数の宣言などをテストに含めることもできます。

#[test]
fn test_with_setup() {
    let input_a = 100;
    let input_b = 200;
    let actual = add(input_a, input_b);

    assert_eq!(actual, input_a + input_b);
}

ユニットテストテストのサンプル 2

次に、カスタム構造体 Point を使用した以下のようなシンプルなコードを実装します。

use std::fmt;

pub struct Point {
    pub x: f64,
    pub y: f64,
}

impl Point {
    pub fn new(x: f64, y: f64) -> Self {
        Self { x, y }
    }

    pub fn origin() -> Self {
        Self { x: 0.0, y: 0.0 }
    }

    pub fn distance_from_origin(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
    
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {

    // new を使って Point を作成
    let p1 = Point::new(3.0, 4.0);
    println!("p1 の座標 = {}", p1);

    // origin を使って原点を作成
    let origin = Point::origin();
    println!("原点の座標 = {}", origin);


    // distance_from_origin を使って原点からの距離を計算
    let distance = p1.distance_from_origin();
    println!("p1 の原点からの距離 = {}", distance);

    println!("\n");
}

このコードに対して、例えば以下のようにユニットテスト内でロードしたカスタム構造体 Point を使用することができます。

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

    #[test]
    fn test_main_ne_custom_struct() {
        let p = Point::new(1.0, 1.0);
        assert_ne!(p, Point::origin());
    }

}

ただし、このコードで上記のテストを実行すると、assert_ne! の依存関係にある Debug や PartialEq の実装が存在しないためテストがエラーとなってしまいます。

image-20260531224028164

このように assert_ne! などのマクロをカスタム構造体のテストに使用する場合は、構造体に #[derive(Debug,PartialEq)] を付けて実装を導出しておく必要があります。

ユニットテストテストのサンプル 3

ここまではテスト関数内でパニックが発生した場合にテストが失敗していましたが、意図的にパニックを発生させたい関数について、「適切にパニックが発生するか」をテストすることもできます。

例えば、以下のような 0 除算が発生した場合に panic をトリガーする除算関数をテストするとします。

pub fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("ゼロで除算することはできません: {} / {}", a, b);
    }
    a / b
}

この場合には、以下のように #[should_panic] アトリビュートをテスト関数に付与します。

#[test]
#[should_panic]
fn test_divide_by_zero_panics() {
    divide(10, 0); // パニックするので、テストは成功
}

このテストが実行された場合には、0 除算によるパニックが発生すればテストが成功となり、逆にパニックが発生せず divide 関数による除算が成功するとテストは失敗となります。

参考:Unit testing - Rust By Example

また、#[should_panic(expected = "message")] のようにパニック発生時のメッセージをアトリビュートで指定することもできます。

ユニットテストテストのサンプル 4

以下はドキュメントのサンプルのそのまま引用ですが、#[ignore] アトリビュートを記述することで、特定のテスト関数を cargo test 時に実行させないようにすることができます。

このような除外されたテストについては cargo test -- --ignored で実行することができます。

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

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

    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
    }

    #[test]
    fn test_add_hundred() {
        assert_eq!(add(100, 2), 102);
        assert_eq!(add(2, 100), 102);
    }

    #[test]
    #[ignore]
    fn ignored_test() {
        assert_eq!(add(0, 0), 0);
    }
}

その他ユニットテストの注意点

Rust のテストは、デフォルトでは複数スレッドで並列実行されるようです。

そのため、複数のテスト間でファイルや DB などのリソースを共有する必要がある場合には適切に Mutex などによりリソースを保護するなどの注意が必要です。

テスト時の並列実行を無効化したい場合は cargo test -- --test-threads=1 を使用できます。

参考:テストの実行のされ方を制御する - The Rust Programming Language 日本語版

また、テストの実行順序は保証されていないため、複数のテスト間に依存関係が発生しないように注意する必要があります。

結合テスト

結合テストの基本構造

Rust の結合テストは、クレートの外側から、ユーザーと同じ視点でプログラムの公開されたインターフェースのみを使用してテストを行います。

結合テストは、ライブラリの各モジュールの連携が正しく動作するかをテストする目的で行われます。

Cargo は、以下のように src ディレクトリと並んで配置された tests ディレクトリを結合テストとして扱います。

my_project/
├── Cargo.toml
├── src/
│   ├── lib.rs          # ライブラリ本体
│   └── utils.rs
└── tests/              # 結合テスト専用ディレクトリ
    ├── api_test.rs     # 個別のテストファイル(各自が独立クレート)
    ├── edge_cases.rs
    └── common/         # テスト共有ユーティリティ
        └── mod.rs      # mod.rs にすることでテストファイルと認識されないため、テスト用のヘルパーコードを配置できる

参考:インテグレーションテスト - Rust By Example 日本語版

結合テストのサンプル 1

今回は、Claude に適当に作らせた電卓ライブラリに対する結合テストを作成します。

クライアントプログラムからこのライブラリを使用する場合、アプリケーションは rust_integration_test_samples クレートの各種モジュールを以下のようにインポートする必要があります。

use rust_integration_test_samples::calculator::Calculator;
use rust_integration_test_samples::storage::{Storage, StorageValue};
use rust_integration_test_samples::user::{Role, UserService};

これは、結合テストを行うテストコードについても同様で、テストコード側でもクライアントプログラムを実装する場合と同じく外部からライブラリを利用するためにモジュールのインポートを行う必要があります。

以下は最もシンプルな結合テストの実装です。

以下では、クライアントプログラムと同様にライブラリの Calculator モジュールをインポートし、test_calculator_basic_operations というテスト関数で add や subtract、multiply などの関数の戻り値をテストしています。

use rust_integration_test_samples::calculator::Calculator;

#[test]
fn test_calculator_basic_operations() {
    let mut calc = Calculator::new();

    assert_eq!(calc.add(2.0, 3.0), 5.0);
    assert_eq!(calc.subtract(10.0, 4.0), 6.0);
    assert_eq!(calc.multiply(3.0, 7.0), 21.0);
}

このテストコードは、tests 配下に配置したファイルに記述します。

my_project/
├── Cargo.toml
├── src/
│   ├── lib.rs          # ライブラリ本体
│   └── utils.rs
└── tests/              # 結合テスト専用ディレクトリ
    └── api_test.rs     # 個別のテストファイル(各自が独立クレート)

結合テストのサンプル 2

同じテストコード内で、以下のように正常系や異常系のテストを行うこともできます。

/// 除算のテスト(正常系)
#[test]
fn test_calculator_division() {
    let mut calc = Calculator::new();

    let result = calc.divide(10.0, 2.0);
    assert_eq!(result, Ok(5.0));
}

/// 除算のテスト(エラー系:ゼロ除算)
#[test]
fn test_calculator_division_by_zero() {
    let mut calc = Calculator::new();

    let result = calc.divide(10.0, 0.0);
    assert_eq!(result, Err(CalcError::DivisionByZero));
}

ライブラリ側の divide 関数は以下のように実装されており戻り値として Result<f64, CalcError> を返すため、結合テスト側のテストコードでも assert_eq!(result, Ok(5.0));assert_eq!(result, Err(CalcError::DivisionByZero)); による結果の評価を行っています。

pub fn divide(&mut self, a: f64, b: f64) -> Result<f64, CalcError> {
    if b == 0.0 {
        return Err(CalcError::DivisionByZero);
    }
    let result = a / b;
    self.record(format!("{} / {}", a, b), result);
    Ok(result)
}

結合テストのサンプル 3

結合テストでは、ユーザーがクライアントプログラムを操作した場合と同じように一連の操作後の状態を順にテストすることもできます。

例えば以下の例では、ライブラリ側で管理する計算履歴の情報に対して、計算の実行後や履歴のクリア後のカウント数をテストしたり、履歴のテスト後も計算を継続することが可能であることを確認するテストを実装しています。

#[test]
fn test_calculator_history_management() {
    let mut calc = Calculator::new();

    // 初期状態
    assert_eq!(calc.history().len(), 0);

    // 操作を実行
    calc.add(1.0, 2.0);
    calc.multiply(3.0, 4.0);
    assert_eq!(calc.history().len(), 2);

    // 履歴クリア
    calc.clear_history();
    assert_eq!(calc.history().len(), 0);

    // クリア後も操作は可能
    calc.add(5.0, 6.0);
    assert_eq!(calc.history().len(), 1);
}

まとめ

ユニットテストや結合テストにはもっと色々な記法があり、より複雑なテストの実装も可能なようですが、とりあえず今回はここまでにします。

最近は AI 使ってばっかりなのでもっとテストをガリガリ書けるようになりたいですね。