Rust Built-in Testing
Rust Built-in Testing (標準のテスト機能)
概要
Rust標準ライブラリに組み込まれた基本的なテスト機能です。#[test]アトリビュートを使用してユニットテストを記述し、cargo testコマンドで実行できます。外部クレートへの依存なしに、シンプルな構文でテストを作成でき、アサーションマクロ(assert!, assert_eq!, assert_ne!など)を提供します。Rustのすべてのテストフレームワークの基盤として機能し、所有権システムと型安全性を活かした信頼性の高いテストが可能です。
詳細
主な特徴
- ゼロ依存: 標準ライブラリのみで動作し、外部クレート不要
- シンプルな構文:
#[test]アトリビュートでテスト関数を定義 - 並列実行: デフォルトでテストを並列に実行し、高速なテストサイクルを実現
- アサーションマクロ:
assert!,assert_eq!,assert_ne!による直感的な検証 - パニックテスト:
#[should_panic]による期待されるパニックの検証 - テスト組織化:
#[cfg(test)]モジュールによるテストコードの分離 - ドキュメントテスト: コメント内のコード例を自動的にテスト実行
アーキテクチャ
Rustのテストシステムは以下のコンポーネントで構成されています:
- Test Runner:
cargo testがコンパイルして実行するテストハーネス - Test Attributes:
#[test],#[should_panic],#[ignore]などのマーカー - Assertion Macros: テスト条件を検証するマクロ群
- Test Organization: ユニットテスト、統合テスト、ドキュメントテストの分離
テストの配置
- ユニットテスト:
srcディレクトリ内のモジュールに#[cfg(test)]付きで配置 - 統合テスト:
tests/ディレクトリに独立したファイルとして配置 - ドキュメントテスト: ドキュメントコメント内のコード例を自動テスト
メリット・デメリット
メリット
- 標準装備: Rustをインストールすれば即座に利用可能
- 学習容易: シンプルで直感的な構文、公式ドキュメントが充実
- ゼロオーバーヘッド: 外部依存がなく、ビルド時間や複雑さが増加しない
- 所有権統合: Rustの所有権システムと完全に統合され、安全なテストを保証
- 並列実行: デフォルトで並列実行し、高速なフィードバックサイクル
- ドキュメント連携: コード例がテストとして機能し、常に最新の状態を保証
デメリット
- 基本機能のみ: パラメータ化テストやプロパティベーステストは別クレートが必要
- アサーション: より高度なマッチャーやカスタムメッセージには制限あり
- テストフィクスチャ: セットアップ/ティアダウンの仕組みが限定的
- 並列制御: テスト間でリソース共有する場合、並列実行の制御が必要
参考ページ
書き方の例
基本的なテスト
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
}
}
アサーションマクロの使用
#[cfg(test)]
mod tests {
#[test]
fn test_assertions() {
// 真偽値のテスト
assert!(true);
assert!(!false);
// 等価性のテスト
assert_eq!(2 + 2, 4);
assert_ne!(2 + 2, 5);
// カスタムメッセージ
let result = 2 + 2;
assert_eq!(
result, 4,
"計算結果が期待値と異なります: 期待値 {}, 実際の値 {}",
4, result
);
}
}
パニックのテスト
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("ゼロ除算は許可されていません");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn test_divide_by_zero() {
divide(10, 0);
}
#[test]
#[should_panic(expected = "ゼロ除算")]
fn test_divide_by_zero_with_message() {
divide(10, 0);
}
}
Result型を使用したテスト
#[cfg(test)]
mod tests {
#[test]
fn test_with_result() -> Result<(), String> {
let result = 2 + 2;
if result == 4 {
Ok(())
} else {
Err(String::from("計算結果が正しくありません"))
}
}
}
テストの無視
#[cfg(test)]
mod tests {
#[test]
fn quick_test() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// 時間のかかるテスト
// 通常は実行されない
// cargo test -- --ignored で実行可能
}
}
統合テスト
// tests/integration_test.rs
use my_crate;
#[test]
fn test_public_api() {
let result = my_crate::add(2, 3);
assert_eq!(result, 5);
}
ドキュメントテスト
/// 2つの数値を加算します。
///
/// # Examples
///
/// ```
/// use my_crate::add;
///
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
ベンチマークテスト (nightly required)
#![feature(test)]
extern crate test;
#[cfg(test)]
mod tests {
use super::*;
use test::Bencher;
#[bench]
fn bench_add(b: &mut Bencher) {
b.iter(|| {
add(2, 3)
});
}
}
並列実行の制御
// 並列実行を無効化: cargo test -- --test-threads=1
// 出力を表示: cargo test -- --nocapture
// 特定のテストを実行: cargo test test_name
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use std::sync::Arc;
#[test]
fn test_with_shared_resource() {
// 共有リソースを使用する場合は、
// Mutex等で同期を取る
let counter = Arc::new(Mutex::new(0));
let mut num = counter.lock().unwrap();
*num += 1;
assert_eq!(*num, 1);
}
}