引言:AI 生成的代码靠谱吗?
使用 Claude Code 一段时间后,你可能遇到过这些情况:
- 场景 1: AI 实现了登录功能,你运行后发现按钮点击无反应------原来事件监听器写错了
- 场景 2: AI 写的 API 返回数据,但前端解析失败------字段名大小写不一致
- 场景 3: Android 应用看起来正常,但提交测试后 QA 发现一堆 UI 错位问题
核心问题: AI 生成代码后,你需要手动验证功能是否正常,这个过程:
- ⏰ 耗时:每次都要手动测试各个功能点
- 🐛 易错:人工测试容易遗漏边界情况
- 🔄 重复:相同的测试动作反复执行
💡 解决方案:自我验证机制
如果 AI 能够自己检查自己生成的代码会怎样?
makefile
传统流程:
AI 生成代码 → 你手动测试 → 发现问题 → 让 AI 修复 → 再次测试
自我验证流程:
AI 生成代码 → AI 自动测试 → 发现问题 → AI 自动修复 → 再次测试
↓
直到所有测试通过 ✅
本文核心内容:
- 自我验证的核心思想与价值
- 验证工具的配置(测试框架、质量检查)
- 前端自动化验证实战(Playwright 真实点击测试)
- 后端 API 自动化验证
- Android 自动化测试(Espresso + UI Automator)
- 集成到开发流程(Hook + 验证)
- 闭环反馈机制
"让 AI 自己检查代码质量,人类负责验收结果"
一、为什么需要自我验证?
1.1 AI 生成代码的常见问题
即使是强大的 Claude Code,也会犯这些错误:
问题 1: 语法正确但逻辑错误
kotlin
// AI 生成的登录验证代码
fun validateLogin(username: String, password: String): Boolean {
// ❌ 逻辑错误:空字符串也会通过验证
return username.isNotEmpty() || password.isNotEmpty()
// 应该是 && 而不是 ||
}
后果: 编译通过,但功能异常。
问题 2: 未考虑边界情况
kotlin
// AI 生成的列表处理代码
fun getFirstItem(list: List<String>): String {
return list[0] // ❌ 如果 list 为空会崩溃
}
后果: 正常情况运行正常,特殊情况崩溃。
问题 3: UI 实现与设计不符
xml
<!-- AI 生成的布局 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户名"
android:textSize="14sp" />
<!-- ❌ 设计要求 16sp,但 AI 写成了 14sp -->
后果: 视觉效果不符合设计规范。
问题 4: API 调用参数错误
kotlin
// AI 生成的 API 调用
val response = api.getUserInfo(
userId = "123",
fields = "name,email" // ❌ API 文档要求用数组而非字符串
)
后果: API 返回错误,功能无法正常工作。
1.2 人工验证的成本
传统人工验证流程:
markdown
1. 运行应用 → 2 分钟(启动、编译)
2. 点击登录按钮测试 → 1 分钟
3. 测试错误场景(密码错误) → 1 分钟
4. 测试边界情况(空输入) → 1 分钟
5. 检查 UI 是否符合设计 → 2 分钟
6. 检查日志是否有错误 → 1 分钟
--------------------------------
单次验证: 8 分钟
如果需要修复 3 次:
8 分钟 × 3 = 24 分钟 ⏰
问题:
- 每次修改后都要重复相同的手动测试
- 容易遗漏边界情况
- 人工成本高,效率低
1.3 自动化验证的价值
配置自动化验证后:
markdown
首次配置自动化脚本: 30 分钟
之后每次验证:
1. AI 生成代码 → 5 分钟
2. AI 自动运行测试 → 2 分钟
- 启动浏览器/应用
- 执行测试用例
- 检查结果
3. AI 自动修复问题(如果测试失败) → 3 分钟
4. AI 再次测试 → 2 分钟
--------------------------------
自动验证: 12 分钟 (完全自动化)
人工介入: 0 分钟 ✅
投资回报:
- 首次投入: 30 分钟
- 每次节省: 24 - 0 = 24 分钟
- 回本: 2 次使用后(30 / 24 ≈ 1.25)
核心价值:
- ✅ 质量保障: 自动化测试覆盖更全面
- ⏰ 效率提升: 无需人工重复测试
- 🔄 闭环反馈: AI 自己发现并修复问题
- 📊 可追踪: 测试结果自动记录
二、自我验证的核心思想
2.1 测试驱动开发(TDD)在 AI 时代的应用
传统 TDD 流程:
markdown
1. 编写测试用例(定义预期行为)
2. 运行测试(必然失败,因为功能未实现)
3. 编写最小可行代码(让测试通过)
4. 重构代码(保持测试通过)
5. 重复上述流程
AI 辅助的 TDD 流程:
markdown
1. 你定义功能需求和验收标准
2. AI 自动生成测试用例
3. AI 实现功能代码
4. AI 运行测试验证
5. 如果测试失败,AI 自动修复
6. 重复步骤 4-5 直到测试通过
2.2 验证即开发流程的一部分

核心理念:
- 验证不是开发完成后的额外步骤
- 验证是开发流程的内置环节
- AI 负责实现和验证,人类负责定义标准和验收结果
2.3 闭环反馈机制
makefile
输入: 功能需求 + 验收标准
│
▼
AI 实现代码
│
▼
AI 自动测试 ──────► 测试报告
│ │
│ ▼
│ 测试失败? ──Yes──┐
│ │ │
│ No │
▼ │ │
交付结果 ◄──────────────┘ │
│
▼
AI 分析失败原因
│
▼
AI 修复代码
│
▼
重新测试 ──┘
关键要素:
- 明确的验收标准: 告诉 AI 什么是"正确"
- 自动化的测试: AI 能够自己运行测试
- 反馈机制: 测试失败时,AI 知道如何修复
- 迭代优化: 不断循环直到测试通过
三、验证工具的配置
3.1 前端测试框架配置
选择 Playwright
为什么选择 Playwright:
- ✅ 支持真实浏览器环境(Chrome、Firefox、Safari)
- ✅ 自动等待元素加载(减少 flaky 测试)
- ✅ 支持截图和录屏(便于调试)
- ✅ API 简洁易用
- ✅ 支持 Headless 模式(CI/CD 友好)
安装 Playwright
bash
# 安装 Playwright
npm install -D @playwright/test
# 安装浏览器驱动
npx playwright install
基础配置
typescript
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
3.2 后端测试工具配置
API 测试工具选择
推荐组合:
- Jest: 测试框架
- Supertest: HTTP 请求测试
- MockK/Mockito: 依赖模拟
安装依赖
bash
# Node.js 后端
npm install -D jest supertest @types/jest @types/supertest
# Kotlin 后端(Android)
// build.gradle.kts
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
3.3 Android 测试框架配置
Espresso + UI Automator
kotlin
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
// Espresso - UI 测试
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1")
// UI Automator - 跨应用测试
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
// Test rules and runners
androidTestImplementation("androidx.test:runner:1.5.2")
androidTestImplementation("androidx.test:rules:1.5.0")
// JUnit
androidTestImplementation("androidx.test.ext:junit:1.1.5")
}
3.4 代码质量检查工具
ESLint + Prettier (前端)
json
{
"devDependencies": {
"eslint": "^8.0.0",
"prettier": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0"
},
"scripts": {
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"verify": "npm run lint && npm run format && npm test"
}
}
ktlint (Android)
kotlin
plugins {
id("org.jlleitschuh.gradle.ktlint") version "11.6.1"
}
ktlint {
android.set(true)
ignoreFailures.set(false)
reporters {
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN)
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
}
}
四、前端自动化验证实战
4.1 验证流程设计
前端自动化验证的完整流程:
markdown
1. 启动本地开发服务器
2. 启动 Playwright 浏览器
3. 导航到测试页面
4. 执行测试动作:
- 点击按钮
- 输入表单
- 提交数据
5. 验证结果:
- 检查 UI 元素是否正确显示
- 检查数据是否正确提交
- 检查控制台是否有错误
6. 生成测试报告
7. 如果测试失败,返回错误信息给 AI
4.2 登录功能验证示例
测试用例定义
typescript
import { test, expect } from '@playwright/test';
test.describe('登录功能测试', () => {
test.beforeEach(async ({ page }) => {
// 每个测试前访问登录页面
await page.goto('/login');
});
test('成功登录', async ({ page }) => {
// 1. 输入用户名和密码
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
// 2. 点击登录按钮
await page.click('button[type="submit"]');
// 3. 验证跳转到首页
await expect(page).toHaveURL('/dashboard');
// 4. 验证欢迎信息显示
await expect(page.locator('.welcome-message')).toContainText('欢迎, testuser');
});
test('用户名为空时显示错误', async ({ page }) => {
// 1. 只输入密码,用户名留空
await page.fill('input[name="password"]', 'password123');
// 2. 点击登录按钮
await page.click('button[type="submit"]');
// 3. 验证错误提示显示
await expect(page.locator('.error-message')).toBeVisible();
await expect(page.locator('.error-message')).toContainText('请输入用户名');
// 4. 验证仍在登录页面
await expect(page).toHaveURL('/login');
});
test('密码错误时显示错误', async ({ page }) => {
// 1. 输入正确用户名和错误密码
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'wrongpassword');
// 2. 点击登录按钮
await page.click('button[type="submit"]');
// 3. 验证错误提示
await expect(page.locator('.error-message')).toContainText('用户名或密码错误');
});
test('检查表单验证', async ({ page }) => {
// 1. 直接点击登录按钮(所有字段为空)
await page.click('button[type="submit"]');
// 2. 验证 HTML5 表单验证触发
const usernameInput = page.locator('input[name="username"]');
expect(await usernameInput.evaluate(el => (el as HTMLInputElement).validationMessage))
.toBeTruthy();
});
});
4.3 检查 UI 布局和样式
typescript
import { test, expect } from '@playwright/test';
test.describe('登录页面 UI 测试', () => {
test('检查布局是否正确', async ({ page }) => {
await page.goto('/login');
// 1. 检查登录卡片居中
const loginCard = page.locator('.login-card');
const box = await loginCard.boundingBox();
const viewport = page.viewportSize();
expect(box).toBeTruthy();
expect(viewport).toBeTruthy();
// 验证水平居中(允许 50px 误差)
const centerX = viewport!.width / 2;
const cardCenterX = box!.x + box!.width / 2;
expect(Math.abs(centerX - cardCenterX)).toBeLessThan(50);
// 2. 检查标题文字大小
const title = page.locator('h1');
const fontSize = await title.evaluate(el =>
window.getComputedStyle(el).fontSize
);
expect(fontSize).toBe('24px');
// 3. 检查按钮颜色
const submitButton = page.locator('button[type="submit"]');
const bgColor = await submitButton.evaluate(el =>
window.getComputedStyle(el).backgroundColor
);
expect(bgColor).toBe('rgb(59, 130, 246)'); // Tailwind blue-500
});
test('检查响应式布局', async ({ page }) => {
// 1. 测试移动端布局
await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE
await page.goto('/login');
const loginCard = page.locator('.login-card');
const box = await loginCard.boundingBox();
// 验证移动端宽度占满(减去 padding)
expect(box!.width).toBeGreaterThan(335); // 375 - 40px padding
// 2. 测试桌面端布局
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto('/login');
const box2 = await loginCard.boundingBox();
// 验证桌面端固定宽度
expect(box2!.width).toBe(400);
});
});
4.4 检查控制台错误
typescript
import { test, expect } from '@playwright/test';
test.describe('控制台错误检查', () => {
test('登录流程无控制台错误', async ({ page }) => {
const errors: string[] = [];
// 监听控制台错误
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
// 监听页面错误
page.on('pageerror', error => {
errors.push(error.message);
});
// 执行登录流程
await page.goto('/login');
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
// 验证无错误
expect(errors).toHaveLength(0);
});
});
4.5 完整的验证脚本
typescript
import { chromium, Browser, Page } from '@playwright/test';
export async function verifyLoginFeature() {
let browser: Browser | null = null;
let page: Page | null = null;
try {
// 1. 启动浏览器
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
// 2. 收集错误
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', error => errors.push(error.message));
// 3. 执行测试
await page.goto('http://localhost:3000/login');
// 测试正常登录
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
// 4. 验证结果
const welcomeVisible = await page.locator('.welcome-message').isVisible();
// 5. 检查布局
const loginCard = page.locator('.login-card');
const box = await loginCard.boundingBox();
const layoutCorrect = box !== null && box.width === 400;
// 6. 生成报告
return {
success: errors.length === 0 && welcomeVisible && layoutCorrect,
errors,
details: {
welcomeVisible,
layoutCorrect,
consoleErrors: errors.length,
},
};
} catch (error) {
return {
success: false,
errors: [(error as Error).message],
details: {},
};
} finally {
await page?.close();
await browser?.close();
}
}
// 命令行调用
if (require.main === module) {
verifyLoginFeature().then(result => {
console.log(JSON.stringify(result, null, 2));
process.exit(result.success ? 0 : 1);
});
}
使用方式:
bash
# Claude Code 可以直接调用此脚本
npm run verify
# 或
node tests/helpers/verify.ts
五、后端 API 自动化验证
5.1 API 测试策略

markdown
API 测试层次:
1. 单元测试 - 测试单个函数/方法
2. 集成测试 - 测试 API 端点
3. 端到端测试 - 测试完整业务流程
5.2 Node.js API 测试示例
typescript
import request from 'supertest';
import app from '../../src/app';
import { createTestDatabase, clearTestDatabase } from '../helpers/db';
describe('Authentication API', () => {
beforeAll(async () => {
await createTestDatabase();
});
afterAll(async () => {
await clearTestDatabase();
});
describe('POST /api/auth/login', () => {
it('应该成功登录并返回 token', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
username: 'testuser',
password: 'password123',
})
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveProperty('token');
expect(response.body).toHaveProperty('user');
expect(response.body.user.username).toBe('testuser');
});
it('用户名错误应返回 401', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
username: 'wronguser',
password: 'password123',
})
.expect(401);
expect(response.body).toHaveProperty('error');
expect(response.body.error).toContain('用户名或密码错误');
});
it('缺少必填字段应返回 400', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
username: 'testuser',
// 缺少 password
})
.expect(400);
expect(response.body).toHaveProperty('error');
});
it('应该验证 token 格式', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
username: 'testuser',
password: 'password123',
});
const token = response.body.token;
expect(token).toMatch(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/); // JWT 格式
});
});
describe('GET /api/user/profile', () => {
let authToken: string;
beforeEach(async () => {
// 先登录获取 token
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
username: 'testuser',
password: 'password123',
});
authToken = loginResponse.body.token;
});
it('有效 token 应返回用户信息', async () => {
const response = await request(app)
.get('/api/user/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('username');
expect(response.body.username).toBe('testuser');
});
it('无 token 应返回 401', async () => {
await request(app)
.get('/api/user/profile')
.expect(401);
});
it('无效 token 应返回 401', async () => {
await request(app)
.get('/api/user/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});
});
5.3 Android Kotlin API 测试示例
kotlin
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.Assert.*
class AuthApiTest {
private val mockApi = mockk<AuthApi>()
private val authRepository = AuthRepository(mockApi)
@Test
fun `login should return user on success`() = runTest {
// Given
val username = "testuser"
val password = "password123"
val expectedUser = User(id = "1", username = username)
coEvery {
mockApi.login(username, password)
} returns Response.success(LoginResponse(
token = "fake-token",
user = expectedUser
))
// When
val result = authRepository.login(username, password)
// Then
assertTrue(result.isSuccess)
assertEquals(expectedUser, result.getOrNull()?.user)
coVerify { mockApi.login(username, password) }
}
@Test
fun `login should return error on invalid credentials`() = runTest {
// Given
coEvery {
mockApi.login(any(), any())
} returns Response.error(401, "Unauthorized".toResponseBody())
// When
val result = authRepository.login("wrong", "wrong")
// Then
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is AuthException)
}
@Test
fun `login should handle network error`() = runTest {
// Given
coEvery {
mockApi.login(any(), any())
} throws IOException("Network error")
// When
val result = authRepository.login("user", "pass")
// Then
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is IOException)
}
}
六、Android 自动化测试实战
6.1 Espresso UI 测试
kotlin
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.*
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Test
fun testSuccessfulLogin() {
// 1. 输入用户名和密码
onView(withId(R.id.usernameEditText))
.perform(typeText("testuser"), closeSoftKeyboard())
onView(withId(R.id.passwordEditText))
.perform(typeText("password123"), closeSoftKeyboard())
// 2. 点击登录按钮
onView(withId(R.id.loginButton))
.perform(click())
// 3. 验证跳转到主页
onView(withId(R.id.mainActivity))
.check(matches(isDisplayed()))
// 4. 验证欢迎消息显示
onView(withId(R.id.welcomeTextView))
.check(matches(withText("欢迎, testuser")))
}
@Test
fun testEmptyUsernameShowsError() {
// 1. 只输入密码
onView(withId(R.id.passwordEditText))
.perform(typeText("password123"), closeSoftKeyboard())
// 2. 点击登录按钮
onView(withId(R.id.loginButton))
.perform(click())
// 3. 验证错误提示显示
onView(withId(R.id.usernameInputLayout))
.check(matches(hasErrorText("请输入用户名")))
}
@Test
fun testInvalidCredentialsShowsError() {
// 1. 输入错误的凭据
onView(withId(R.id.usernameEditText))
.perform(typeText("wronguser"), closeSoftKeyboard())
onView(withId(R.id.passwordEditText))
.perform(typeText("wrongpass"), closeSoftKeyboard())
// 2. 点击登录按钮
onView(withId(R.id.loginButton))
.perform(click())
// 3. 等待网络请求完成
Thread.sleep(2000)
// 4. 验证错误提示
onView(withId(R.id.errorTextView))
.check(matches(isDisplayed()))
.check(matches(withText(containsString("用户名或密码错误"))))
}
@Test
fun testUILayoutCorrect() {
// 1. 检查标题显示正确
onView(withId(R.id.titleTextView))
.check(matches(isDisplayed()))
.check(matches(withText("用户登录")))
// 2. 检查输入框存在
onView(withId(R.id.usernameEditText))
.check(matches(isDisplayed()))
onView(withId(R.id.passwordEditText))
.check(matches(isDisplayed()))
// 3. 检查登录按钮存在且可点击
onView(withId(R.id.loginButton))
.check(matches(isDisplayed()))
.check(matches(isEnabled()))
}
}
6.2 UI Automator 跨应用测试
kotlin
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.*
import org.junit.Before
import org.junit.Test
class LoginFlowTest {
private lateinit var device: UiDevice
@Before
fun setUp() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
// 启动应用
val context = InstrumentationRegistry.getInstrumentation().context
val intent = context.packageManager.getLaunchIntentForPackage("com.example.app")
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
device.wait(Until.hasObject(By.pkg("com.example.app")), 3000)
}
@Test
fun testCompleteLoginFlow() {
// 1. 找到并点击用户名输入框
val usernameField = device.findObject(
UiSelector()
.resourceId("com.example.app:id/usernameEditText")
)
usernameField.waitForExists(3000)
usernameField.text = "testuser"
// 2. 找到并点击密码输入框
val passwordField = device.findObject(
UiSelector()
.resourceId("com.example.app:id/passwordEditText")
)
passwordField.text = "password123"
// 3. 点击登录按钮
val loginButton = device.findObject(
UiSelector()
.resourceId("com.example.app:id/loginButton")
)
loginButton.click()
// 4. 等待跳转
device.wait(Until.hasObject(By.text("欢迎")), 5000)
// 5. 验证主页显示
val welcomeText = device.findObject(
UiSelector().textContains("欢迎, testuser")
)
assertTrue(welcomeText.exists())
}
@Test
fun testScreenRotation() {
// 1. 输入用户名
val usernameField = device.findObject(
UiSelector().resourceId("com.example.app:id/usernameEditText")
)
usernameField.text = "testuser"
// 2. 旋转屏幕
device.setOrientationLeft()
device.waitForIdle()
// 3. 验证输入保留
val usernameAfterRotation = device.findObject(
UiSelector().resourceId("com.example.app:id/usernameEditText")
)
assertEquals("testuser", usernameAfterRotation.text)
// 4. 恢复屏幕方向
device.setOrientationNatural()
}
}
七、集成到开发流程
7.1 在 claude.md 中定义验证要求
markdown
# 自我验证要求
## 前端功能开发
每次完成前端功能开发后,必须执行以下验证:
1. **代码质量检查**
```bash
npm run lint
npm run format
-
自动化测试
bashnpm run test -
E2E 测试
bashnpm run test:e2e -
验证步骤
- 运行
npm run verify启动完整验证流程 - 检查测试报告,确保所有测试通过
- 如果发现问题,立即分析原因并修复
- 重新运行验证,直到所有测试通过
- 运行
-
验证标准
- ✅ 所有单元测试通过
- ✅ 所有 E2E 测试通过
- ✅ 无 ESLint 错误
- ✅ 代码格式符合 Prettier 规范
- ✅ 控制台无错误信息
- ✅ UI 布局符合设计规范
Android 功能开发
每次完成 Android 功能开发后,必须执行:
-
代码质量检查
bash./gradlew ktlintCheck ./gradlew detekt -
单元测试
bash./gradlew test -
UI 测试
bash./gradlew connectedAndroidTest -
验证标准
- ✅ 所有测试通过
- ✅ ktlint 检查通过
- ✅ 无 Detekt 警告
- ✅ UI 测试覆盖核心流程
API 开发
每次完成 API 开发后,必须:
-
单元测试
bashnpm run test:unit -
集成测试
bashnpm run test:integration -
API 文档更新
- 更新 Swagger/OpenAPI 文档
- 更新 README 中的 API 示例
重要原则
- 测试失败不交付: 只有所有验证通过才算完成
- 自动修复: 发现问题后自动分析并修复,无需人工介入
- 详细报告: 生成详细的测试报告,便于追踪问题
bash
### 7.2 配置 Hook 自动触发验证
```bash title="~/.claude/hooks/auto-verify.sh"
#!/bin/bash
TASK_DESC="$1"
TASK_STATUS="$2"
PROJECT_DIR="$3"
# 只在任务完成时触发验证
if [ "$TASK_STATUS" != "completed" ]; then
exit 0
fi
# 判断项目类型
if [ -f "$PROJECT_DIR/package.json" ]; then
# Node.js 项目
echo "🔍 开始验证前端代码..."
cd "$PROJECT_DIR"
npm run verify
if [ $? -ne 0 ]; then
echo "❌ 验证失败,请检查错误并修复"
exit 1
fi
echo "✅ 前端验证通过"
elif [ -f "$PROJECT_DIR/gradlew" ]; then
# Android 项目
echo "🔍 开始验证 Android 代码..."
cd "$PROJECT_DIR"
./gradlew ktlintCheck test
if [ $? -ne 0 ]; then
echo "❌ 验证失败,请检查错误并修复"
exit 1
fi
echo "✅ Android 验证通过"
fi
exit 0
配置到 settings.json:
json
{
"hooks": {
"task-complete": "~/.claude/hooks/auto-verify.sh"
}
}
7.3 验证失败的反馈机制
bash
#!/bin/bash
PROJECT_DIR="$1"
MAX_RETRIES=3
RETRY_COUNT=0
cd "$PROJECT_DIR"
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
echo "🔍 第 $((RETRY_COUNT + 1)) 次验证..."
# 运行测试
npm run verify > verify-result.log 2>&1
if [ $? -eq 0 ]; then
echo "✅ 验证通过!"
rm verify-result.log
exit 0
fi
# 测试失败,分析错误
echo "❌ 验证失败,分析错误..."
# 提取错误信息
ERROR_SUMMARY=$(grep -A 5 "FAIL" verify-result.log || grep -A 5 "Error" verify-result.log)
# 将错误信息返回给 Claude Code
echo "发现以下错误:"
echo "$ERROR_SUMMARY"
# 如果是最后一次尝试,退出
if [ $RETRY_COUNT -eq $((MAX_RETRIES - 1)) ]; then
echo "❌ 已达到最大重试次数,请人工检查"
exit 1
fi
# 等待 AI 修复(实际中 AI 会自动分析并修复)
echo "等待修复..."
sleep 5
RETRY_COUNT=$((RETRY_COUNT + 1))
done
八、最佳实践与常见问题
8.1 测试编写最佳实践
原则 1: 测试应该简单易懂
typescript
// ❌ 不好的测试:逻辑复杂
test('login', async ({ page }) => {
const users = ['user1', 'user2', 'user3'];
for (const user of users) {
await page.fill('input', user);
// ...复杂的条件判断
}
});
// ✅ 好的测试:清晰直接
test('user1 login successfully', async ({ page }) => {
await page.fill('input[name="username"]', 'user1');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
原则 2: 避免测试间依赖
typescript
// ❌ 不好:测试间有依赖
test('create user', async () => {
await createUser('testuser');
});
test('login user', async () => {
// 依赖上一个测试创建的用户
await login('testuser', 'password');
});
// ✅ 好:每个测试独立
test('login user', async () => {
// 在测试内部创建需要的数据
await createUser('testuser');
await login('testuser', 'password');
});
原则 3: 使用有意义的测试名称
typescript
// ❌ 不好
test('test1', () => { /* ... */ });
test('should work', () => { /* ... */ });
// ✅ 好
test('should show error when username is empty', () => { /* ... */ });
test('should redirect to dashboard after successful login', () => { /* ... */ });
8.2 常见问题与解决方案
问题 1: 测试不稳定(Flaky Tests)
症状: 同一个测试有时通过,有时失败
原因:
- 网络请求延迟
- 元素加载时间不确定
- 异步操作未正确等待
解决方案:
typescript
// ❌ 不好:固定等待时间
await page.click('button');
await page.waitForTimeout(2000); // 可能不够或浪费时间
// ✅ 好:等待具体条件
await page.click('button');
await page.waitForSelector('.success-message', { state: 'visible' });
问题 2: 测试运行太慢
原因:
- 启动浏览器/应用慢
- 串行执行测试
- 每个测试都重新创建环境
解决方案:
typescript
// playwright.config.ts
export default defineConfig({
fullyParallel: true, // 并行运行测试
workers: 4, // 4 个并发 worker
retries: 1, // 失败重试 1 次
use: {
trace: 'on-first-retry', // 只在重试时记录 trace
},
});
问题 3: 测试覆盖率低
解决方案:
bash
# 生成覆盖率报告
npm run test -- --coverage
# 设置覆盖率目标
# package.json
{
"jest": {
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
8.3 性能优化技巧
技巧 1: 使用 beforeAll 共享setup
typescript
describe('User API', () => {
let authToken: string;
beforeAll(async () => {
// 只登录一次,所有测试共享 token
const response = await request(app)
.post('/api/auth/login')
.send({ username: 'test', password: 'test' });
authToken = response.body.token;
});
test('get profile', async () => {
await request(app)
.get('/api/user/profile')
.set('Authorization', `Bearer ${authToken}`);
});
test('update profile', async () => {
await request(app)
.put('/api/user/profile')
.set('Authorization', `Bearer ${authToken}`);
});
});
技巧 2: 使用测试数据工厂
typescript
// tests/factories/user.factory.ts
export function createTestUser(overrides = {}) {
return {
id: generateId(),
username: 'testuser_' + Date.now(),
email: 'test@example.com',
password: 'password123',
...overrides,
};
}
// 使用
test('create user', async () => {
const user = createTestUser({ username: 'alice' });
await createUser(user);
});
九、总结与展望
9.1 核心收益回顾
通过本文的学习,你掌握了:
-
自我验证的价值
- AI 生成代码的常见问题
- 人工验证的成本
- 自动化验证的投资回报率
-
验证工具配置
- Playwright (前端 E2E 测试)
- Jest + Supertest (后端 API 测试)
- Espresso + UI Automator (Android 测试)
- 代码质量检查工具
-
实战技能
- 编写前端自动化测试(真实点击、布局检查、控制台错误)
- 编写后端 API 测试(单元测试、集成测试)
- 编写 Android UI 测试(Espresso、UI Automator)
- 集成到 Claude Code 工作流(claude.md + Hook)
-
最佳实践
- 测试编写原则
- 常见问题解决
- 性能优化技巧
相关资源
官方文档
社区资源
工具推荐
- Playwright Test for VSCode - VSCode 插件
- Testing Library - 用户行为测试库
- Testcontainers - 容器化测试环境
🔗 相关文章:
如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题或建议,欢迎在评论区留言讨论。让我们一起学习,一起成长!
也欢迎访问我的个人主页发现更多宝藏资源