自我验证机制:让 AI 自己检查代码质量

引言:AI 生成的代码靠谱吗?

使用 Claude Code 一段时间后,你可能遇到过这些情况:

  • 场景 1: AI 实现了登录功能,你运行后发现按钮点击无反应------原来事件监听器写错了
  • 场景 2: AI 写的 API 返回数据,但前端解析失败------字段名大小写不一致
  • 场景 3: Android 应用看起来正常,但提交测试后 QA 发现一堆 UI 错位问题

核心问题: AI 生成代码后,你需要手动验证功能是否正常,这个过程:

  • ⏰ 耗时:每次都要手动测试各个功能点
  • 🐛 易错:人工测试容易遗漏边界情况
  • 🔄 重复:相同的测试动作反复执行

💡 解决方案:自我验证机制

如果 AI 能够自己检查自己生成的代码会怎样?

makefile 复制代码
传统流程:
AI 生成代码 → 你手动测试 → 发现问题 → 让 AI 修复 → 再次测试

自我验证流程:
AI 生成代码 → AI 自动测试 → 发现问题 → AI 自动修复 → 再次测试
                                                  ↓
                                      直到所有测试通过 ✅

本文核心内容:

  1. 自我验证的核心思想与价值
  2. 验证工具的配置(测试框架、质量检查)
  3. 前端自动化验证实战(Playwright 真实点击测试)
  4. 后端 API 自动化验证
  5. Android 自动化测试(Espresso + UI Automator)
  6. 集成到开发流程(Hook + 验证)
  7. 闭环反馈机制

"让 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)

核心价值:

  1. 质量保障: 自动化测试覆盖更全面
  2. 效率提升: 无需人工重复测试
  3. 🔄 闭环反馈: AI 自己发现并修复问题
  4. 📊 可追踪: 测试结果自动记录

二、自我验证的核心思想

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 修复代码
                                    │
                                    ▼
                          重新测试 ──┘

关键要素:

  1. 明确的验收标准: 告诉 AI 什么是"正确"
  2. 自动化的测试: AI 能够自己运行测试
  3. 反馈机制: 测试失败时,AI 知道如何修复
  4. 迭代优化: 不断循环直到测试通过

三、验证工具的配置

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
  1. 自动化测试

    bash 复制代码
    npm run test
  2. E2E 测试

    bash 复制代码
    npm run test:e2e
  3. 验证步骤

    • 运行 npm run verify 启动完整验证流程
    • 检查测试报告,确保所有测试通过
    • 如果发现问题,立即分析原因并修复
    • 重新运行验证,直到所有测试通过
  4. 验证标准

    • ✅ 所有单元测试通过
    • ✅ 所有 E2E 测试通过
    • ✅ 无 ESLint 错误
    • ✅ 代码格式符合 Prettier 规范
    • ✅ 控制台无错误信息
    • ✅ UI 布局符合设计规范

Android 功能开发

每次完成 Android 功能开发后,必须执行:

  1. 代码质量检查

    bash 复制代码
    ./gradlew ktlintCheck
    ./gradlew detekt
  2. 单元测试

    bash 复制代码
    ./gradlew test
  3. UI 测试

    bash 复制代码
    ./gradlew connectedAndroidTest
  4. 验证标准

    • ✅ 所有测试通过
    • ✅ ktlint 检查通过
    • ✅ 无 Detekt 警告
    • ✅ UI 测试覆盖核心流程

API 开发

每次完成 API 开发后,必须:

  1. 单元测试

    bash 复制代码
    npm run test:unit
  2. 集成测试

    bash 复制代码
    npm run test:integration
  3. 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 核心收益回顾

通过本文的学习,你掌握了:

  1. 自我验证的价值

    • AI 生成代码的常见问题
    • 人工验证的成本
    • 自动化验证的投资回报率
  2. 验证工具配置

    • Playwright (前端 E2E 测试)
    • Jest + Supertest (后端 API 测试)
    • Espresso + UI Automator (Android 测试)
    • 代码质量检查工具
  3. 实战技能

    • 编写前端自动化测试(真实点击、布局检查、控制台错误)
    • 编写后端 API 测试(单元测试、集成测试)
    • 编写 Android UI 测试(Espresso、UI Automator)
    • 集成到 Claude Code 工作流(claude.md + Hook)
  4. 最佳实践

    • 测试编写原则
    • 常见问题解决
    • 性能优化技巧

相关资源

官方文档

社区资源

工具推荐


🔗 相关文章:


如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题或建议,欢迎在评论区留言讨论。让我们一起学习,一起成长!

也欢迎访问我的个人主页发现更多宝藏资源

相关推荐
laplace01233 小时前
temperature定义与使用
agent·claude·rag·mcp·skills
烁烁闪闪烁烁4 小时前
【weelinking系列Claude教程】 04 - Claude Code 安装与配置
人工智能·chatgpt·ai编程·claude·cursor·claude code·opencode
ServBay4 小时前
GLM-5 拉高开源上限,离一人公司更近了
aigc·ai编程
相思半5 小时前
告别聊天机器人!2026 智能体元年:Claude 4.6 vs GPT-5.3 vs OpenClaw 全方位对比
人工智能·gpt·深度学习·claude·codex·智能体·seedance
玉梅小洋5 小时前
2026年2月大模型性能对比分析报告
人工智能·ai·大模型·ai编程·ai工具
程序员陆业聪5 小时前
让 AI 帮你写代码?先学会跟它说话
ai编程
滑板上的老砒霜6 小时前
AI 共舞,还是被“注意力刺客”偷袭?——程序员的数字专注力守护指南
android·ai编程·客户端
github.com/starRTC6 小时前
Claude Code中英文系列教程33:用魔法打败魔法,利用官方Skill创建Skill
ai编程
github.com/starRTC7 小时前
Claude Code中英文系列教程31:使用 MCP 里面的资源
ai编程