| name | test-development |
| description | Tauri 项目测试开发技能,覆盖 Rust 单元测试和 React 组件测试。
触发场景:
- 需要为 Rust Command 编写测试
- 需要为 React 组件编写测试
- 需要设计测试策略
- 需要运行和调试测试
触发词:测试、test、单元测试、集成测试、TDD、测试用例
|
Tauri 测试开发
测试策略
┌──────────────────┐
│ E2E 测试 │ (可选: Playwright/WebdriverIO)
│ 完整应用流程 │
┌┴──────────────────┴┐
│ 集成测试 │ (Rust: Command + Service + Database)
│ 模块间交互 │
┌┴────────────────────┴┐
│ 单元测试 │ (Rust: cargo test / TS: Vitest)
│ 函数/组件级别 │
└──────────────────────┘
Rust 三层架构测试
本项目采用三层架构(models → database → services → commands),测试应覆盖各层。
测试金字塔
┌─────────────┐
│ Command 测试 │ 集成测试(调用 Service)
┌┴──────────────┴┐
│ Service 测试 │ 业务逻辑测试(调用 Database)
┌┴─────────────────┴┐
│ Database 测试 │ 单元测试(纯函数)
└───────────────────┘
Database 层测试(单元测试)
示例:测试数据库操作
use rusqlite::{Connection, Result};
use crate::models::User;
pub fn create_table(conn: &Connection) -> Result<()> {
conn.execute(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL
)",
[],
)?;
Ok(())
}
pub fn insert_user(conn: &Connection, user: &User) -> Result<()> {
conn.execute(
"INSERT INTO users (name, email) VALUES (?1, ?2)",
[&user.name, &user.email],
)?;
Ok(())
}
pub fn get_user(conn: &Connection, id: i64) -> Result<User> {
conn.query_row(
"SELECT id, name, email FROM users WHERE id = ?1",
[id],
|row| Ok(User {
id: row.get(0)?,
name: row.get(1)?,
email: row.get(2)?,
})
)
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::Connection;
fn setup_test_db() -> Connection {
let conn = Connection::open_in_memory().unwrap();
create_table(&conn).unwrap();
conn
}
#[test]
fn test_insert_and_get_user() {
let conn = setup_test_db();
let user = User {
id: 0,
name: "Alice".into(),
email: "alice@example.com".into(),
};
insert_user(&conn, &user).unwrap();
let retrieved = get_user(&conn, 1).unwrap();
assert_eq!(retrieved.name, "Alice");
assert_eq!(retrieved.email, "alice@example.com");
}
#[test]
fn test_get_nonexistent_user() {
let conn = setup_test_db();
let result = get_user(&conn, 999);
assert!(result.is_err());
}
}
Service 层测试(业务逻辑测试)
示例:测试业务逻辑
use crate::database;
use crate::models::User;
use crate::error::AppError;
use rusqlite::Connection;
pub fn validate_email(email: &str) -> Result<(), AppError> {
if !email.contains('@') || !email.contains('.') {
return Err(AppError::InvalidInput("无效的邮箱格式".into()));
}
Ok(())
}
pub fn add_user(conn: &Connection, name: &str, email: &str) -> Result<(), AppError> {
validate_email(email)?;
let user = User {
id: 0,
name: name.into(),
email: email.into(),
};
database::user::insert_user(conn, &user)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::Connection;
fn setup_test_db() -> Connection {
let conn = Connection::open_in_memory().unwrap();
database::user::create_table(&conn).unwrap();
conn
}
#[test]
fn test_validate_email_valid() {
assert!(validate_email("test@example.com").is_ok());
}
#[test]
fn test_validate_email_invalid() {
assert!(validate_email("invalid").is_err());
assert!(validate_email("no-at-sign.com").is_err());
assert!(validate_email("no-dot@com").is_err());
}
#[test]
fn test_add_user_success() {
let conn = setup_test_db();
let result = add_user(&conn, "Alice", "alice@example.com");
assert!(result.is_ok());
}
#[test]
fn test_add_user_invalid_email() {
let conn = setup_test_db();
let result = add_user(&conn, "Bob", "invalid-email");
assert!(result.is_err());
if let Err(AppError::InvalidInput(msg)) = result {
assert!(msg.contains("邮箱"));
}
}
}
Command 层测试(集成测试)
示例:测试 Tauri Command
use crate::services;
use crate::models::User;
#[tauri::command]
pub fn add_user(name: String, email: String) -> Result<(), String> {
let conn = get_connection().map_err(|e| e.to_string())?;
services::user::add_user(&conn, &name, &email)
.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_user(id: i64) -> Result<User, String> {
let conn = get_connection().map_err(|e| e.to_string())?;
database::user::get_user(&conn, id)
.map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_user_command() {
let result = add_user("Alice".into(), "alice@example.com".into());
assert!(result.is_ok());
}
#[test]
fn test_add_user_command_invalid() {
let result = add_user("Bob".into(), "invalid".into());
assert!(result.is_err());
}
}
运行 Rust 测试
cd src-tauri && cargo test
cd src-tauri && cargo test database::user::tests
cd src-tauri && cargo test test_add_user
cd src-tauri && cargo test -- --nocapture
cd src-tauri && cargo test
cd src-tauri && cargo test -- --test-threads=1
React 测试 (Vitest)
安装
pnpm add -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: "./src/test/setup.ts",
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
组件测试
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import UserPage from "./UserPage";
vi.mock("@tauri-apps/api/core", () => ({
invoke: vi.fn(),
}));
describe("UserPage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders user list", async () => {
const { invoke } = await import("@tauri-apps/api/core");
(invoke as ReturnType<typeof vi.fn>).mockResolvedValue([
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
]);
render(<UserPage />);
await waitFor(() => {
expect(screen.getByText("Alice")).toBeInTheDocument();
expect(screen.getByText("Bob")).toBeInTheDocument();
});
});
it("adds a new user", async () => {
const { invoke } = await import("@tauri-apps/api/core");
(invoke as ReturnType<typeof vi.fn>).mockResolvedValue(null);
render(<UserPage />);
const nameInput = screen.getByPlaceholderText("姓名");
const emailInput = screen.getByPlaceholderText("邮箱");
const submitButton = screen.getByText("添加");
fireEvent.change(nameInput, { target: { value: "Charlie" } });
fireEvent.change(emailInput, { target: { value: "charlie@example.com" } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith("add_user", {
name: "Charlie",
email: "charlie@example.com",
});
});
});
it("handles error when adding user", async () => {
const { invoke } = await import("@tauri-apps/api/core");
(invoke as ReturnType<typeof vi.fn>).mockRejectedValue("无效的邮箱格式");
render(<UserPage />);
const nameInput = screen.getByPlaceholderText("姓名");
const emailInput = screen.getByPlaceholderText("邮箱");
const submitButton = screen.getByText("添加");
fireEvent.change(nameInput, { target: { value: "Invalid" } });
fireEvent.change(emailInput, { target: { value: "invalid" } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/邮箱格式/)).toBeInTheDocument();
});
});
});
API 测试
import { describe, it, expect, vi, beforeEach } from "vitest";
import { api } from "./index";
vi.mock("@tauri-apps/api/core", () => ({
invoke: vi.fn(),
}));
describe("API", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("calls getUser command", async () => {
const { invoke } = await import("@tauri-apps/api/core");
(invoke as ReturnType<typeof vi.fn>).mockResolvedValue({
id: 1,
name: "Alice",
email: "alice@example.com",
});
const user = await api.getUser(1);
expect(invoke).toHaveBeenCalledWith("get_user", { id: 1 });
expect(user.name).toBe("Alice");
});
});
运行前端测试
pnpm vitest
pnpm vitest run
pnpm vitest run --coverage
pnpm vitest src/pages/UserPage.test.tsx
测试最佳实践
Rust 测试
- 使用内存数据库:测试时使用
Connection::open_in_memory() 避免文件冲突
- 独立测试:每个测试函数独立,不依赖其他测试
- 测试边界情况:正常情况 + 错误情况 + 边界情况
- 使用 setup 函数:提取通用的测试准备代码
- 测试三层分别:Database → Service → Command 分层测试
React 测试
- Mock Tauri API:使用
vi.mock 模拟 @tauri-apps/api/core
- 等待异步:使用
waitFor 等待异步操作完成
- 清理 Mock:每个测试前用
beforeEach 清理 mock
- 测试用户交互:使用
fireEvent 或 @testing-library/user-event
- 测试错误处理:测试 API 调用失败的情况
常见错误
| 错误做法 | 正确做法 |
|---|
| 不写测试直接提交 | 至少为核心功能编写单元测试 |
| 前端测试中真实调用 invoke | Mock @tauri-apps/api/core |
| 只测试正常路径 | 同时测试错误路径 (Err/异常) |
| 测试中硬编码文件路径 | 使用 Connection::open_in_memory() |
| 不测试 Database 层 | Database 层最容易出 bug,必须测试 |
| 不测试 Service 业务逻辑 | Service 层包含关键业务逻辑,必须测试 |
| Command 测试不充分 | Command 是前端入口,需要集成测试 |
| 测试之间相互依赖 | 每个测试独立,不依赖其他测试 |