一键导入
test-development
Tauri 项目测试开发技能,覆盖 Rust 单元测试和 React 组件测试。 触发场景: - 需要为 Rust Command 编写测试 - 需要为 React 组件编写测试 - 需要设计测试策略 - 需要运行和调试测试 触发词:测试、test、单元测试、集成测试、TDD、测试用例
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Tauri 项目测试开发技能,覆盖 Rust 单元测试和 React 组件测试。 触发场景: - 需要为 Rust Command 编写测试 - 需要为 React 组件编写测试 - 需要设计测试策略 - 需要运行和调试测试 触发词:测试、test、单元测试、集成测试、TDD、测试用例
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Git 工作流与版本管理技能,规范分支策略、提交信息和发布流程。 触发场景: - 用户需要创建分支或合并代码 - 用户需要规范提交信息格式 - 用户需要管理版本发布流程 触发词:Git、分支、提交、合并、版本发布
发布 Tauri 桌面应用新版本,处理版本号同步、Git tag、GitHub Actions 构建、Release 仓库产物同步、Cloudflare R2 上传、update.json 生成、自动更新发布和文档站重建。 触发场景: - 需要发布新版本或 release - 需要打 tag、推送发布分支或触发 CI 构建 - 需要生成或更新 update.json - 需要处理 Tauri updater 签名产物 - 需要同步 R2 CDN、GitHub/Gitee release 仓库或发布安装包 触发词:发布、release、版本发布、推送、打Tag、update.json、签名构建、安装包、自动更新、R2、CDN
Git 工作流与版本管理技能,规范分支策略、提交信息和发布流程。 触发场景: - 用户需要创建分支或合并代码 - 用户需要规范提交信息格式 - 用户需要管理版本发布流程 触发词:Git、分支、提交、合并、版本发布
自主连续开发方法论(Codex 版)。把一个项目按「每轮一个最小可提交单元」的方式推进到完成: 每轮读真相源 → 选下一个未做项 → 做一个最小单元 → 过验证门 → 记录勾选 → 最小提交 → 报进度。 适用于"按需求/台账建功能、还原原型、修一批 bug、补测试、重构、迁移、清理"等需要反复同构推进的任务。 与 Claude Code 的关系:Claude Code 有内置 `/loop` 自动续期(`/loop /dev-loop`),Codex 没有—— 所以 Codex 端由你(或脚本 / cron)反复触发"继续下一轮",方法论本身一致。 触发场景: - 想让 Codex 把某个项目/某个意图"一轮轮推进到完成" - 还原原型图、修一批 warning、补单测、重构模块等同构重复任务 - 需要"每步可编译、可回退、可恢复"的稳健推进方式 触发词:自主开发、连续开发、循环开发、一轮一提交、还原原型、批量修复、autonomous dev、continuous dev
Tauri 打包与分发技能,指导桌面端(Windows/macOS/Linux)与移动端(Android/iOS)的构建、签名和分发。 触发场景: - 需要构建生产安装包(exe/dmg/deb/apk/aab/ipa) - 需要配置各平台打包参数 - 需要代码签名(Windows 证书 / Android keystore / iOS 描述文件) - 需要减小安装包体积 - 需要设置应用图标和元数据 - 需要打包 Android / iOS App 触发词: 打包、构建、build、发布、安装包、exe、dmg、deb、apk、aab、ipa、签名、分发、release、android build、ios build、移动端打包
排查已发生的问题、定位 Bug 原因。 触发场景: - 代码运行报错,需要定位原因 - 功能不正常,需要排查 - Tauri Command 返回错误,需要分析 - 日志分析、调试代码 触发词:Bug、报错、不工作、调试、排查、为什么、出问题、失败、不生效、无效、找不到原因、定位问题
| name | test-development |
| description | Tauri 项目测试开发技能,覆盖 Rust 单元测试和 React 组件测试。 触发场景: - 需要为 Rust Command 编写测试 - 需要为 React 组件编写测试 - 需要设计测试策略 - 需要运行和调试测试 触发词:测试、test、单元测试、集成测试、TDD、测试用例 |
┌──────────────────┐
│ E2E 测试 │ (可选: Playwright/WebdriverIO)
│ 完整应用流程 │
┌┴──────────────────┴┐
│ 集成测试 │ (Rust: Command + Service + Database)
│ 模块间交互 │
┌┴────────────────────┴┐
│ 单元测试 │ (Rust: cargo test / TS: Vitest)
│ 函数/组件级别 │
└──────────────────────┘
本项目采用三层架构(models → database → services → commands),测试应覆盖各层。
┌─────────────┐
│ Command 测试 │ 集成测试(调用 Service)
┌┴──────────────┴┐
│ Service 测试 │ 业务逻辑测试(调用 Database)
┌┴─────────────────┴┐
│ Database 测试 │ 单元测试(纯函数)
└───────────────────┘
// src-tauri/src/database/user.rs
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());
}
}
// src-tauri/src/services/user.rs
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("邮箱"));
}
}
}
// src-tauri/src/commands/user.rs
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::*;
// 注意:Command 测试需要模拟完整的应用环境
// 可以直接调用 Command 函数,但需要确保数据库连接可用
#[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
# 显示输出(包括 println!)
cd src-tauri && cargo test -- --nocapture
# 并行运行测试(默认)
cd src-tauri && cargo test
# 串行运行测试(避免数据库冲突)
cd src-tauri && cargo test -- --test-threads=1
pnpm add -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
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"),
},
},
});
// src/pages/UserPage.test.tsx
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import UserPage from "./UserPage";
// Mock Tauri invoke
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();
});
});
});
// src/lib/api/index.test.ts
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");
});
});
# 运行测试(watch 模式)
pnpm vitest
# 运行一次
pnpm vitest run
# 覆盖率
pnpm vitest run --coverage
# 运行特定测试文件
pnpm vitest src/pages/UserPage.test.tsx
Connection::open_in_memory() 避免文件冲突vi.mock 模拟 @tauri-apps/api/corewaitFor 等待异步操作完成beforeEach 清理 mockfireEvent 或 @testing-library/user-event| 错误做法 | 正确做法 |
|---|---|
| 不写测试直接提交 | 至少为核心功能编写单元测试 |
| 前端测试中真实调用 invoke | Mock @tauri-apps/api/core |
| 只测试正常路径 | 同时测试错误路径 (Err/异常) |
| 测试中硬编码文件路径 | 使用 Connection::open_in_memory() |
| 不测试 Database 层 | Database 层最容易出 bug,必须测试 |
| 不测试 Service 业务逻辑 | Service 层包含关键业务逻辑,必须测试 |
| Command 测试不充分 | Command 是前端入口,需要集成测试 |
| 测试之间相互依赖 | 每个测试独立,不依赖其他测试 |