Tauri Rust 测试指南(AI总结)
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,这是已知的平台限制。
二、分层测试策略
┌─────────────────────────────────┐
│ 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() {
// 用内存库,测试结束自动销毁
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.toml 里确保 tokio 开启 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/
基本流程:
- 安装
tauri-driver - 配置 WebdriverIO
- 启动 Tauri app,通过 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] + 内存库 |
✅ |
| 需要 AppHandle | tauri::test::mock_builder |
⚠️ 不稳定 |
| Tauri 命令、emit、窗口 | WebdriverIO e2e | ✅(需打包) |
九、总结
Tauri 测试的核心思路只有一句话:
把业务逻辑从 Tauri 命令里分离出来,命令层只做参数传递,逻辑层用普通 Rust 测试覆盖,运行时相关的用 e2e 兜底。
这样既能保证测试覆盖率,又不被 Tauri 运行时的限制卡住。

浙公网安备 33010602011771号