rstest
rstest
概要
rstestは、Rust用のフィクスチャベーステストフレームワークです。標準の#[test]属性を拡張し、フィクスチャ(テストデータの準備)、パラメータ化テスト、非同期テスト、タイムアウト機能などの高度なテスト機能を提供します。テストコードの重複を削減し、保守性と可読性を大幅に向上させる現代的なRustテストソリューションです。
詳細
主な特徴
- フィクスチャシステム:
#[fixture]によるテストデータの再利用可能な準備 - パラメータ化テスト:
#[case]と#[values]による複数ケースの効率的テスト - 非同期テストサポート:
async関数のネイティブサポート - タイムアウト機能:
#[timeout]によるテスト実行時間制御 - テンプレート機能:
rstest_reuseによる複雑なテストパターンの再利用 - カスタムアサーション: 柔軟なアサーション拡張機能
- 並列実行: 高速なテスト実行のための並列化
- トレース機能: デバッグ用の詳細なテスト情報出力
アーキテクチャ
rstestは以下の核心コンポーネントで構成されています:
- フィクスチャエンジン: テストデータの生成と依存関係管理
- パラメータジェネレーター: テストケースの自動生成機能
- 実行ランナー: 並列実行とタイムアウト制御
- コンテキスト管理: テストメタデータとライフサイクル管理
メリット・デメリット
メリット
- 高い再利用性: フィクスチャによるテストデータの効率的な共有
- 豊富な表現力: パラメータ化テストによる包括的なテストカバレッジ
- 開発効率向上: テンプレート機能による重複コード削減
- 強力なデバッグ: トレース機能による詳細な実行情報
- 非同期対応: 最新のRust非同期プログラミングとの完全統合
- 標準互換性: Rustの標準テストシステムとの seamless統合
デメリット
- 学習コスト: 豊富な機能による初期学習コストの増加
- コンパイル時間: マクロの多用によるわずかなコンパイル時間増加
- 外部依存: 標準ライブラリ以外のクレート依存
- 複雑性: シンプルなテストには過剰な機能
参考ページ
書き方の例
基本的なフィクスチャ
use rstest::*;
#[fixture]
fn sample_data() -> Vec<i32> {
vec![1, 2, 3, 4, 5]
}
#[rstest]
fn test_data_length(sample_data: Vec<i32>) {
assert_eq!(sample_data.len(), 5);
}
#[rstest]
fn test_data_sum(sample_data: Vec<i32>) {
let sum: i32 = sample_data.iter().sum();
assert_eq!(sum, 15);
}
パラメータ化テスト
use rstest::rstest;
#[rstest]
#[case(0, 0)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
#[case(5, 5)]
fn test_fibonacci(#[case] input: u32, #[case] expected: u32) {
assert_eq!(fibonacci(input), expected);
}
// 名前付きケース
#[rstest]
#[case::zero(0, 0)]
#[case::one(1, 1)]
#[case::small_numbers(2, 1)]
#[case::larger_numbers(10, 55)]
fn test_fibonacci_named(#[case] n: u32, #[case] expected: u32) {
assert_eq!(fibonacci(n), expected);
}
バリューリストテスト
#[rstest]
fn test_is_even(
#[values(0, 2, 4, 6, 8, 10)] n: i32
) {
assert!(n % 2 == 0);
}
#[rstest]
fn test_string_validation(
#[values("hello", "world", "rust")] input: &str
) {
assert!(!input.is_empty());
assert!(input.chars().all(|c| c.is_ascii_lowercase()));
}
// 複数のバリューリスト(組み合わせテスト)
#[rstest]
fn test_arithmetic(
#[values(1, 2)] a: i32,
#[values(3, 4)] b: i32
) {
assert!(a + b > 0);
// (1,3), (1,4), (2,3), (2,4) の4パターンでテストされる
}
高度なフィクスチャ
#[fixture]
fn database() -> TestDatabase {
TestDatabase::new()
}
#[fixture]
fn user(#[default("john")] name: &str, #[default(25)] age: u32) -> User {
User::new(name, age)
}
#[rstest]
fn test_default_user(user: User) {
assert_eq!(user.name(), "john");
assert_eq!(user.age(), 25);
}
#[rstest]
fn test_custom_user(#[with("alice", 30)] user: User) {
assert_eq!(user.name(), "alice");
assert_eq!(user.age(), 30);
}
#[rstest]
fn test_partial_custom_user(#[with("bob")] user: User) {
assert_eq!(user.name(), "bob");
assert_eq!(user.age(), 25); // デフォルト値
}
非同期テスト
#[fixture]
async fn async_data() -> Result<String, Error> {
fetch_data_from_api().await
}
#[rstest]
async fn test_async_operation(#[future] async_data: String) {
assert!(!async_data.is_empty());
}
#[rstest]
#[case(1, 2, 3)]
#[case(5, 5, 10)]
async fn test_async_calculation(
#[case] a: i32,
#[case] b: i32,
#[case] expected: i32
) {
let result = async_add(a, b).await;
assert_eq!(result, expected);
}
タイムアウトとエラーハンドリング
use std::time::Duration;
#[rstest]
#[timeout(Duration::from_millis(100))]
async fn test_fast_operation() {
// 100ms以内に完了する必要がある
quick_async_operation().await;
}
#[rstest]
#[case::should_pass(Duration::from_millis(10), 4)]
#[timeout(Duration::from_millis(50))]
#[case::should_timeout(Duration::from_millis(100), 4)]
#[case::should_fail_value(Duration::from_millis(10), 5)]
#[timeout(Duration::from_millis(200))]
async fn test_with_timeout_override(
#[case] delay: Duration,
#[case] expected: u32
) {
let result = delayed_calculation(2, 2, delay).await;
assert_eq!(result, expected);
}
#[rstest]
#[should_panic(expected = "Division by zero")]
fn test_panic_handling() {
divide(10, 0);
}
テンプレートとコード再利用
use rstest_reuse::{self, *};
// テンプレートの定義
#[template]
#[rstest]
#[case(2, 2)]
#[case(4, 4)]
#[case(8, 8)]
fn power_of_two_cases(#[case] input: u32, #[case] expected: u32) {}
// テンプレートの適用
#[apply(power_of_two_cases)]
fn test_is_power_of_two(input: u32, expected: u32) {
assert_eq!(input, expected);
assert!(is_power_of_two(input));
}
#[apply(power_of_two_cases)]
fn test_log_base_two(input: u32, expected: u32) {
assert_eq!(log_base_two(input), expected.trailing_zeros());
}
コンテキストとトレース
use rstest::{rstest, Context};
#[rstest]
#[case::quick_test(42)]
#[case::another_test(100)]
#[trace] // デバッグ情報を有効化
fn test_with_context(#[context] ctx: Context, #[case] value: i32) {
println!("Test: {}", ctx.name);
println!("Description: {:?}", ctx.description);
println!("Case: {:?}", ctx.case);
// テスト実行時間の計測
let start = ctx.start;
std::thread::sleep(std::time::Duration::from_millis(10));
let elapsed = start.elapsed();
assert!(elapsed >= std::time::Duration::from_millis(10));
assert!(value > 0);
}
複雑なフィクスチャ依存関係
#[fixture]
fn database_config() -> DatabaseConfig {
DatabaseConfig::test_config()
}
#[fixture]
fn database(database_config: DatabaseConfig) -> Database {
Database::connect(database_config)
}
#[fixture]
fn user_repository(database: Database) -> UserRepository {
UserRepository::new(database)
}
#[fixture]
fn test_user() -> User {
User::new("test_user", "[email protected]")
}
#[rstest]
fn test_user_crud_operations(
user_repository: UserRepository,
test_user: User
) {
// ユーザー作成
let created_user = user_repository.create(test_user.clone()).unwrap();
assert_eq!(created_user.name(), test_user.name());
// ユーザー取得
let found_user = user_repository.find_by_id(created_user.id()).unwrap();
assert_eq!(found_user.name(), test_user.name());
// ユーザー更新
let updated_user = user_repository.update_email(
created_user.id(),
"[email protected]"
).unwrap();
assert_eq!(updated_user.email(), "[email protected]");
// ユーザー削除
user_repository.delete(created_user.id()).unwrap();
assert!(user_repository.find_by_id(created_user.id()).is_err());
}
ファイルベーステスト
#[rstest]
fn test_config_files(
#[files("tests/configs/*.toml")] path: std::path::PathBuf
) {
let config_content = std::fs::read_to_string(&path).unwrap();
let config: Config = toml::from_str(&config_content).unwrap();
// 設定ファイルの妥当性チェック
assert!(!config.database_url.is_empty());
assert!(config.port > 0);
assert!(config.port < 65536);
}