Tauri Rust テスト実装ガイド

Tauriプロジェクトのテスト戦略は、一般的なRustプロジェクトとは異なる特性を持ちます。最大の問題は、Tauriランタイム(AppHandle、Window、State)をテスト環境에서 直接構築できないことです。本稿では、実用的な階層型テストアプローチを整理します。

一、Tauriテストが特殊な理由

Tauriコマンドは通常 다음과 같이定義されます:

#[tauri::command]
pub async fn create_post(
    state: State<'_, AppState>,
    payload: CreatePostPayload,
) -> Result<Post, String> {
    state.post_service.create(payload).await.map_err(|e| e.to_string())
}

ここで重要なのは、State<'_, AppState> がTauriランタイムによって注入されるものであるため、テストコードから手動で構築することができません。単純に #[test] を記述すると、コンパイルエラーまたはpanicが発生します。

また、Windows環境では cargo test で生成されるバイナリにTauriが必要とするWindows manifestが含まれていないため、STATUS_ENTRYPOINT_NOT_FOUND エラーが発生することです。これは既知のプラットフォーム制限です。

https://github.com/tauri-apps/tauri/issues/13419

二、階層型テスト戦略

┌─────────────────────────────────┐
│         e2e テスト               │  ← WebdriverIO / Selenium
│  Tauri コマンド、emit、ウィンドウ操作 │
├─────────────────────────────────┤
│         統合テスト               │  ← cargo test (tests/ ディレクトリ)
│  データベース、ファイルシステム、Service層 │
├─────────────────────────────────┤
│         ユニットテスト           │  ← cargo test (同ファイル mod tests)
│  純ビジネスロジック、Domain層、ユーティリティ関数 │
└─────────────────────────────────┘

原則:ロジックを分離し、コマンド層は薄く保ちます。

三、ユニットテスト:純ロジックのテスト

ビジネスロジックをコマンドから抽出し、通常の関数として実装します:

// ビジネスロジック:純関数、Tauriに依存しない
pub fn validate_username(name: &str) -> Result<(), String> {
    if name.is_empty() {
        return Err("ユーザー名は空にできません".to_string());
    }
    if name.len() > 32 {
        return Err("ユーザー名は32文字以内にしてください".to_string());
    }
    Ok(())
}

// Tauriコマンド:呼び出しのみ担当
#[tauri::command]
pub fn check_username(name: String) -> Result<(), String> {
    validate_username(&name)
}

テストではビジネスロジックのみを検証します:

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

    #[test]
    fn test_empty_username() {
        assert!(validate_username("").is_err());
    }

    #[test]
    fn test_valid_username() {
        assert!(validate_username("alice").is_ok());
    }

    #[test]
    fn test_too_long_username() {
        let long_name = "a".repeat(33);
        assert!(validate_username(&long_name).is_err());
    }
}

四、統合テスト:Service/データベースのテスト

メモリデータベース(SQLite :memory:)を使用して統合テストを実行し、Tauriランタイムに依存しません:

// tests/post_service_test.rs

use my_app::infrastructure::database::ConnectionPool;
use my_app::application::services::PostService;

#[tokio::test]
async fn test_create_post() {
    // メモリDB使用、テスト終了時に自動解放
    let pool = ConnectionPool::new(":memory:").await.unwrap();
    pool.run_migrations().await.unwrap();

    let service = PostService::new(pool);
    let result = service.create("hello world").await;

    assert!(result.is_ok());
    assert_eq!(result.unwrap().content, "hello world");
}

Cargo.tomltokio のtest featureを有効にします:

[dev-dependencies]
tokio = { version = "1", features = ["full"] }

五、AppHandleが必要な場合:tauri::testの活用

Tauriは mock_builder を提供しており、AppHandleが必要なシナリオのテストに役立ちます:

# Cargo.toml
[dev-dependencies]
tauri = { version = "2", features = ["test"] }
#[cfg(test)]
mod tests {
    use tauri::test::mock_builder;
    use tauri::Manager;

    #[test]
    fn test_state_injection() {
        let app = mock_builder()
            .build(tauri::generate_context!())
            .unwrap();

        app.manage(MyState { count: 0 });

        let state: tauri::State<MyState> = app.state();
        assert_eq!(state.count, 0);
    }
}

⚠️ Windows環境では、この種のテストはmanifest問題により失敗する可能性があります。Linux CIで実行することを推奨します。

六、e2eテスト:Tauriコマンド自体のテスト

ランタイム真正に依存する部分(イベントemit、ウィンドウ操作、完全なコマンド呼び出し)には、WebdriverIOを使用して実際のTauriウィンドウを駆動します。

Tauri公式ドキュメント:https://tauri.app/develop/tests/webdriver/

基本的な流れ:

  1. tauri-driver をインストール
  2. WebdriverIOを設定
  3. Tauriアプリを起動し、WebDriverプロトコルを通じてコマンドを呼び出し、結果をアサーション
cargo install tauri-driver
// wdio.conf.js (簡略化)
export const config = {
    specs: ['./tests/e2e/**/*.spec.js'],
    capabilities: [{
        'tauri:options': {
            application: './src-tauri/target/release/my-app'
        }
    }],
}

七、Windows上の特有の問題

Windowsネイティブ環境で cargo test を実行すると、プロジェクトがTauriランタイムに依存している場合(間接的であっても)、 следующиеエラーが発生します:

error: process didn't exit successfully: STATUS_ENTRYPOINT_NOT_FOUND

根本原因tao(Tauriのウィンドウライブラリ)が comctl32.dll v6 を静的リンクしており、Windows manifestでアクティブ化する必要がありますが、cargo test で生成されるバイナリにはこのmanifestがありません。

解決策

アプローチ 説明
Linux/Dockerでテストを実行 最も安定、CIでの使用を推奨
#\[cfg(not(test))\] でTauri依存を分離 テストバイナリがTauriランタイムをリンクしないようにする
ランタイム非依存の層のみをテスト 根本的な解決策、アーキテクチャ上で分離

プロジェクトでの分離書き込み例:

// lib.rs
#[cfg(not(test))]
mod state;  // stateモジュールはAppHandleに依存、テスト時にはコンパイルしない

#[cfg(not(test))]
mod tauri_app;

八、クイックリファレンス

テスト対象 使用する方法 Windowsで実行可能
純関数、Domainロジック #\[test\]
非同期Service #\[tokio::test\]
データベース操作 #\[tokio::test\] + メモリDB
AppHandleが必要 tauri::test::mock\_builder ⚠️ 不安定
Tauriコマンド、emit、ウィンドウ WebdriverIO e2e ✅(ビルドが必要)

九、まとめ

Tauriテストの中核的なアプローチは、以下の言葉に集約されます:

ビジネスロジックをTauriコマンドから分離し、コMAND層はパラメータ受け渡のみ担当、ロジック層は通常のRustテストでカバー、ランタイム依存部分はe2eで補完する。

これにより、テストカバレッジを確保しながら、Tauriランタイムの制限に惑わされることなく開発を進められます。

タグ: rust Tauri testing integration-testing unit-testing

6月14日 01:03 投稿