npx skills add https://github.com/different-ai/zero-finance --skill testability确保您构建的每个功能都从一开始就可测试。本技能教授:
"如果你不能在本地测试它,你就无法测试它。"
每个功能都应该在多个层级上可测试。为可测试性而设计,而不是事后附加。
从最快(持续运行)到最慢(偶尔运行):
▲
/U\ UI 测试 (E2E)
/ I \ - 浏览器自动化
/-----\ - 仅在预发布环境运行
/ API \ API/集成测试
/ TESTS \ - tRPC 过程
/-----------\ - 可在本地运行
/ UNIT \ 单元测试
/ TESTS \ - 纯函数
/------------------\ - 最快,始终运行
| 层级 | 速度 | 运行环境 | 使用时机 |
|---|
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 单元测试 | <1秒 | 本地 | 纯逻辑、工具函数、计算 |
| API/集成测试 | 1-10秒 | 本地 + CI | tRPC、数据库操作、业务逻辑 |
| 预发布环境测试 | 30秒-2分钟 | Vercel 预览 | 完整流程验证 |
| UI/E2E 测试 | 2-5分钟 | 仅预发布环境 | 关键用户旅程 |
// packages/web/src/lib/utils/calculate-fee.ts
export function calculateFee(amount: number, feePercent: number): number {
return amount * (feePercent / 100);
}
// packages/web/src/lib/utils/calculate-fee.test.ts
import { describe, it, expect } from 'vitest';
import { calculateFee } from './calculate-fee';
describe('calculateFee', () => {
it('calculates 1% fee correctly', () => {
expect(calculateFee(1000, 1)).toBe(10);
});
it('handles zero amount', () => {
expect(calculateFee(0, 5)).toBe(0);
});
});
cd packages/web
pnpm test # 运行所有测试(监视模式)
pnpm test -- --run # 运行一次并退出
pnpm test:watch # 监视模式
pnpm test -- --run --grep "fee" # 按名称过滤
仓库说明:
@zero-finance/web的 Vitest 会发现在packages/web/src/test/**/*.test.ts下的测试。将新测试放在那里(或更新 Vitest 配置),以便它们被识别。
// packages/web/src/server/routers/earn/get-balance.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createTestContext } from '@/test/context';
import { earnRouter } from './index';
describe('earn.getBalance', () => {
let ctx: ReturnType<typeof createTestContext>;
beforeEach(() => {
ctx = createTestContext({
user: { privyDid: 'test-user-did' },
workspaceId: 'test-workspace-id',
});
});
it('returns balance for valid user', async () => {
const caller = earnRouter.createCaller(ctx);
const result = await caller.getBalance({ chainId: 8453 });
expect(result).toHaveProperty('balance');
expect(typeof result.balance).toBe('string');
});
});
// Mock Privy
vi.mock('@privy-io/server-auth', () => ({
PrivyClient: vi.fn().mockImplementation(() => ({
getUser: vi.fn().mockResolvedValue({ id: 'test-user' }),
})),
}));
// Mock Database
vi.mock('@/db', () => ({
db: {
query: {
userSafes: {
findFirst: vi.fn().mockResolvedValue({
safeAddress: '0x1234...',
chainId: 8453,
}),
},
},
},
}));
对于需要真实数据库的测试:
// packages/web/src/test/setup-db.ts
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
export async function createTestDb() {
// 使用特定于测试的数据库
const sql = neon(process.env.TEST_DATABASE_URL!);
return drizzle(sql);
}
# 1. 推送你的分支
git push -u origin feat/my-feature
# 2. 等待部署
LATEST=$(vercel ls --scope prologe 2>/dev/null | head -1)
vercel inspect "$LATEST" --scope prologe --wait --timeout 5m
# 3. 在预览 URL 上测试
# 使用 Chrome MCP 或手动测试
加载 test-staging-branch 技能用于:
Chrome 自动化登录流程
Gmail OTP 提取
PR 报告
skill("test-staging-branch")
// packages/web/tests/dashboard.spec.ts
import { test, expect } from '@playwright/test';
test('user can view dashboard balance', async ({ page }) => {
// 登录将使用测试夹具
await page.goto('/dashboard');
await expect(page.getByText('Total Balance')).toBeVisible();
await expect(page.getByTestId('balance-amount')).toBeVisible();
});
cd packages/web
pnpm exec playwright test
pnpm exec playwright test --ui # 交互模式
// 不好 - 难以测试
export async function getBalance() {
const safe = await db.query.userSafes.findFirst({...});
const balance = await fetch(`https://api.example.com/balance/${safe.address}`);
return balance;
}
// 好 - 可测试
export async function getBalance(
deps: {
getSafe: () => Promise<UserSafe>,
fetchBalance: (address: string) => Promise<string>,
}
) {
const safe = await deps.getSafe();
const balance = await deps.fetchBalance(safe.address);
return balance;
}
// 不好 - 逻辑与 I/O 混合
export async function processTransfer(amount: number) {
const fee = amount * 0.01;
const total = amount + fee;
await db.insert(transfers).values({ amount, fee, total });
return { amount, fee, total };
}
// 好 - 可提取的纯函数
export function calculateTransferFees(amount: number) {
const fee = amount * 0.01;
const total = amount + fee;
return { amount, fee, total };
}
export async function processTransfer(amount: number) {
const calculated = calculateTransferFees(amount);
await db.insert(transfers).values(calculated);
return calculated;
}
// 现在 calculateTransferFees 很容易进行单元测试!
// 为 E2E 测试添加 data-testid
<div data-testid="balance-card">
<span data-testid="balance-amount">{balance}</span>
</div>
通过 API 路由暴露内部状态,这些路由:
仅在开发/测试环境中可用
在生产环境中受保护
// packages/web/src/app/api/test/state/route.ts import { NextResponse } from 'next/server';
export async function GET() { // 仅在开发环境中 if (process.env.NODE_ENV === 'production') { return NextResponse.json({ error: 'Not available' }, { status: 403 }); }
// 返回用于测试的内部状态 return NextResponse.json({ safesCount: await db.select().from(userSafes).count(), // ... 其他调试信息 }); }
# .env.test - 测试专用配置
DATABASE_URL="postgres://test:test@localhost:5432/test_db"
PRIVY_APP_ID="test-app-id"
# ... 模拟值
创建可重用的测试辅助函数:
// packages/web/src/test/fixtures.ts
export const testUser = {
privyDid: 'did:privy:test-user',
email: 'test@example.com',
};
export const testSafe = {
address: '0x1234567890123456789012345678901234567890',
chainId: 8453,
};
// packages/web/src/test/context.ts
export function createTestContext(overrides = {}) {
return {
user: testUser,
workspaceId: 'test-workspace',
db: mockDb,
...overrides,
};
}
在认为一个功能"完成"之前:
[ ] 纯函数的单元测试
[ ] tRPC 过程的集成测试
[ ] 外部服务的模拟
[ ] UI 组件中的测试 ID
[ ] 在预发布环境的手动测试(如果适用)
[ ] 仅关键路径的 E2E 测试
// 不好 - 测试内部状态
expect(component.state.isLoading).toBe(false);
// 好 - 测试可观察行为
expect(screen.getByText('Loading...')).not.toBeVisible();
// 不好 - 模拟所有东西
vi.mock('@/db');
vi.mock('@/lib/api');
vi.mock('@/hooks/use-user');
// ... 还有 10 个模拟
// 好 - 仅模拟外部边界
vi.mock('@/lib/external-api'); // 仅第三方
// 不好 - 为简单验证编写 E2E 测试
test('email validation shows error', async ({ page }) => {
// 这应该是一个单元测试!
});
// 好 - 仅关键流程的 E2E 测试
test('user can complete payment flow', async ({ page }) => {
// 多步骤、多服务流程
});
| 场景 | 使用的技能 |
|---|---|
| 预发布环境测试失败 | test-staging-branch |
| 需要调试生产数据 | debug prod issues |
| 完成测试后 | skill-reinforcement |
| 需要 Chrome 自动化 | chrome-devtools-mcp |
在此处添加新发现的学习内容
params 作为 Promise;在读取 slug 之前 await params 以避免本地 CLI 测试中的 404 错误。wallet_index 和 create_direct_signer;省略默认值,仅在明确提供时发送这些字段。# 单元测试
pnpm --filter @zero-finance/web test
# 监视模式
pnpm --filter @zero-finance/web test:watch
# E2E 测试
pnpm --filter @zero-finance/web exec playwright test
# 类型检查(捕获许多错误)
pnpm typecheck
# 代码检查
pnpm lint
每周安装次数
1
仓库
GitHub 星标数
202
首次出现
1 天前
安全审计
安装在
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1
Ensure every feature you build is testable from the start. This skill teaches:
"If you can't test it locally, you can't test it."
Every feature should be testable at multiple levels. Design for testability, don't bolt it on later.
From fastest (run constantly) to slowest (run occasionally):
▲
/U\ UI Tests (E2E)
/ I \ - Browser automation
/-----\ - Run on staging only
/ API \ API/Integration Tests
/ TESTS \ - tRPC procedures
/-----------\ - Can run locally
/ UNIT \ Unit Tests
/ TESTS \ - Pure functions
/------------------\ - Fastest, run always
| Layer | Speed | Where | When to Use |
|---|---|---|---|
| Unit | <1s | Local | Pure logic, utils, calculations |
| API/Integration | 1-10s | Local + CI | tRPC, DB operations, business logic |
| Staging | 30s-2m | Vercel preview | Full flow verification |
| UI/E2E | 2-5m | Staging only | Critical user journeys |
// packages/web/src/lib/utils/calculate-fee.ts
export function calculateFee(amount: number, feePercent: number): number {
return amount * (feePercent / 100);
}
// packages/web/src/lib/utils/calculate-fee.test.ts
import { describe, it, expect } from 'vitest';
import { calculateFee } from './calculate-fee';
describe('calculateFee', () => {
it('calculates 1% fee correctly', () => {
expect(calculateFee(1000, 1)).toBe(10);
});
it('handles zero amount', () => {
expect(calculateFee(0, 5)).toBe(0);
});
});
cd packages/web
pnpm test # Run all tests (watch mode)
pnpm test -- --run # Run once and exit
pnpm test:watch # Watch mode
pnpm test -- --run --grep "fee" # Filter by name
Repo note:
@zero-finance/webVitest discovers tests underpackages/web/src/test/**/*.test.ts. Put new tests there (or update Vitest config) so they get picked up.
// packages/web/src/server/routers/earn/get-balance.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createTestContext } from '@/test/context';
import { earnRouter } from './index';
describe('earn.getBalance', () => {
let ctx: ReturnType<typeof createTestContext>;
beforeEach(() => {
ctx = createTestContext({
user: { privyDid: 'test-user-did' },
workspaceId: 'test-workspace-id',
});
});
it('returns balance for valid user', async () => {
const caller = earnRouter.createCaller(ctx);
const result = await caller.getBalance({ chainId: 8453 });
expect(result).toHaveProperty('balance');
expect(typeof result.balance).toBe('string');
});
});
// Mock Privy
vi.mock('@privy-io/server-auth', () => ({
PrivyClient: vi.fn().mockImplementation(() => ({
getUser: vi.fn().mockResolvedValue({ id: 'test-user' }),
})),
}));
// Mock Database
vi.mock('@/db', () => ({
db: {
query: {
userSafes: {
findFirst: vi.fn().mockResolvedValue({
safeAddress: '0x1234...',
chainId: 8453,
}),
},
},
},
}));
For tests that need a real database:
// packages/web/src/test/setup-db.ts
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
export async function createTestDb() {
// Use a test-specific database
const sql = neon(process.env.TEST_DATABASE_URL!);
return drizzle(sql);
}
# 1. Push your branch
git push -u origin feat/my-feature
# 2. Wait for deployment
LATEST=$(vercel ls --scope prologe 2>/dev/null | head -1)
vercel inspect "$LATEST" --scope prologe --wait --timeout 5m
# 3. Test on preview URL
# Use Chrome MCP or manual testing
Load the test-staging-branch skill for:
Chrome automation login flow
Gmail OTP extraction
PR reporting
skill("test-staging-branch")
// packages/web/tests/dashboard.spec.ts
import { test, expect } from '@playwright/test';
test('user can view dashboard balance', async ({ page }) => {
// Login would use test fixtures
await page.goto('/dashboard');
await expect(page.getByText('Total Balance')).toBeVisible();
await expect(page.getByTestId('balance-amount')).toBeVisible();
});
cd packages/web
pnpm exec playwright test
pnpm exec playwright test --ui # Interactive mode
// BAD - Hard to test
export async function getBalance() {
const safe = await db.query.userSafes.findFirst({...});
const balance = await fetch(`https://api.example.com/balance/${safe.address}`);
return balance;
}
// GOOD - Testable
export async function getBalance(
deps: {
getSafe: () => Promise<UserSafe>,
fetchBalance: (address: string) => Promise<string>,
}
) {
const safe = await deps.getSafe();
const balance = await deps.fetchBalance(safe.address);
return balance;
}
// BAD - Logic mixed with I/O
export async function processTransfer(amount: number) {
const fee = amount * 0.01;
const total = amount + fee;
await db.insert(transfers).values({ amount, fee, total });
return { amount, fee, total };
}
// GOOD - Pure function extractable
export function calculateTransferFees(amount: number) {
const fee = amount * 0.01;
const total = amount + fee;
return { amount, fee, total };
}
export async function processTransfer(amount: number) {
const calculated = calculateTransferFees(amount);
await db.insert(transfers).values(calculated);
return calculated;
}
// Now calculateTransferFees is easily unit testable!
// Add data-testid for E2E tests
<div data-testid="balance-card">
<span data-testid="balance-amount">{balance}</span>
</div>
Expose internal state via API routes that are:
Only available in development/test
Protected in production
// packages/web/src/app/api/test/state/route.ts import { NextResponse } from 'next/server';
export async function GET() { // Only in development if (process.env.NODE_ENV === 'production') { return NextResponse.json({ error: 'Not available' }, { status: 403 }); }
// Return internal state for testing return NextResponse.json({ safesCount: await db.select().from(userSafes).count(), // ... other debug info }); }
# .env.test - Test-specific config
DATABASE_URL="postgres://test:test@localhost:5432/test_db"
PRIVY_APP_ID="test-app-id"
# ... mocked values
Create reusable test helpers:
// packages/web/src/test/fixtures.ts
export const testUser = {
privyDid: 'did:privy:test-user',
email: 'test@example.com',
};
export const testSafe = {
address: '0x1234567890123456789012345678901234567890',
chainId: 8453,
};
// packages/web/src/test/context.ts
export function createTestContext(overrides = {}) {
return {
user: testUser,
workspaceId: 'test-workspace',
db: mockDb,
...overrides,
};
}
Before considering a feature "done":
[ ] Unit tests for pure functions
[ ] Integration tests for tRPC procedures
[ ] Mocks for external services
[ ] Test IDs in UI components
[ ] Manual test on staging (if applicable)
[ ] E2E test for critical paths only
// BAD - Tests internal state
expect(component.state.isLoading).toBe(false);
// GOOD - Tests observable behavior
expect(screen.getByText('Loading...')).not.toBeVisible();
// BAD - Mock everything
vi.mock('@/db');
vi.mock('@/lib/api');
vi.mock('@/hooks/use-user');
// ... 10 more mocks
// GOOD - Mock only external boundaries
vi.mock('@/lib/external-api'); // Third-party only
// BAD - E2E for simple validation
test('email validation shows error', async ({ page }) => {
// This should be a unit test!
});
// GOOD - E2E for critical flows only
test('user can complete payment flow', async ({ page }) => {
// Multi-step, multi-service flow
});
| Scenario | Skill to Use |
|---|---|
| Testing fails on staging | test-staging-branch |
| Need to debug prod data | debug prod issues |
| After completing tests | skill-reinforcement |
| Need Chrome automation | chrome-devtools-mcp |
Add new learnings here as they're discovered
params as a Promise; await params before reading slug to avoid 404s in local CLI testing.wallet_index and create_direct_signer; omit defaults and only send those fields when explicitly provided.# Unit tests
pnpm --filter @zero-finance/web test
# Watch mode
pnpm --filter @zero-finance/web test:watch
# E2E tests
pnpm --filter @zero-finance/web exec playwright test
# Type check (catches many bugs)
pnpm typecheck
# Lint
pnpm lint
Weekly Installs
1
Repository
GitHub Stars
202
First Seen
1 day ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
109,600 周安装