AI Agent 前端工作流(二):智能代码审查与自动化测试

AI Agent 前端工作流(二):智能代码审查与自动化测试

回顾:从组件生成到质量保障

上一篇文章我们聊了用 AI Agent 生成 React 组件,从产品需求到可运行代码,效率提升了 3-5 倍。但是,代码能跑和代码质量高是两回事。

生产环境的代码需要经过严格的审查和测试。传统上,这两个环节都是人力密集型工作:

  • Code Review:高级工程师花大量时间审查初级工程师的代码,但很难做到全面、一致
  • 写测试:测试覆盖率常年在 30%-50% 徘徊,大家都知道应该写,但总是"等下次有时间再补"

这篇文章我们要解决的问题是:AI Agent 能不能承担这些质量保障工作?

答案是:能,而且效果超预期。

一、AI 驱动的代码审查

传统 Code Review 的三大困境

让我们诚实一点:

  1. 精力有限:资深工程师审 10 个 PR 后就会疲劳,后面的 PR 质量难保证
  2. 标准不一:同一个问题,周一可能被指出,周五可能就被放过了
  3. 覆盖不全:人很难在审查时同时关注性能、安全、可访问性、最佳实践......太多维度了

结果就是:要么 Review 成本高昂(每个 PR 30 分钟+),要么流于形式(点个 LGTM 了事)。

AI Agent 的优势

AI 不会累,不会情绪化,而且可以同时检查几十个维度。更重要的是:它 24/7 在线,成本可控

但关键是怎么集成到工作流里。我的方案是:Git Hook + AI API

实战:集成到 Git Hook

我们用 Husky 在 pre-commit 阶段触发 AI 代码审查。完整实现如下:

1. 安装依赖
bash 复制代码
npm install -D husky lint-staged
npx husky install
2. 创建审查脚本

scripts/ai-code-review.js

javascript 复制代码
#!/usr/bin/env node
import { execSync } from 'child_process';
import Anthropic from '@anthropic-ai/sdk';
import fs from 'fs';
import path from 'path';

const client = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY
});

// 获取暂存区的文件变更
function getStagedDiff() {
  try {
    return execSync('git diff --cached', { encoding: 'utf-8' });
  } catch (error) {
    console.error('无法获取 git diff:', error.message);
    return '';
  }
}

// AI 审查 Prompt(这是关键)
const REVIEW_PROMPT = \`你是一个资深前端工程师,负责代码审查。请审查以下 git diff,关注:

**性能问题:**
- 不必要的重渲染(缺少 useMemo、useCallback)
- 大数组操作没有做虚拟化
- 图片/资源未优化
- 阻塞主线程的同步操作

**安全隐患:**
- XSS 风险(dangerouslySetInnerHTML 未做转义)
- CSRF 防护缺失
- 敏感信息泄露(API key、token 硬编码)
- eval() 或 Function() 构造器的使用

**最佳实践:**
- 组件职责是否单一
- props 类型检查(TypeScript / PropTypes)
- 错误边界处理
- 可访问性(aria 属性、语义化标签)
- 命名规范和代码风格

**边界情况:**
- 空数组、null、undefined 的处理
- 异步操作的错误处理
- 网络请求失败的降级方案

请按以下格式输出:

## 🚨 Critical Issues(阻断性问题)
- [文件名:行号] 问题描述 + 修复建议

## ⚠️ Warnings(需要关注)
- [文件名:行号] 问题描述 + 优化建议

## ✅ Good Practices(做得好的地方)
- 简要列出亮点

## 📊 Summary
- 总体评分(1-10)
- 是否建议合并

如果没有发现任何问题,输出 "✅ LGTM - 代码质量良好,建议合并"\`;

async function reviewCode(diff) {
  if (!diff || diff.trim().length === 0) {
    console.log('✅ 没有代码变更需要审查');
    return { shouldBlock: false, report: '' };
  }

  console.log('🤖 AI 正在审查代码...\\n');

  try {
    const message = await client.messages.create({
      model: 'claude-sonnet-4-5',
      max_tokens: 4096,
      messages: [{
        role: 'user',
        content: \`\${REVIEW_PROMPT}\\n\\n## Git Diff:\\n\\\`\\\`\\\`diff\\n\${diff}\\n\\\`\\\`\\\`\`
      }]
    });

    const report = message.content[0].text;
    
    // 保存审查报告
    const reportPath = path.join(process.cwd(), '.ai-review-report.md');
    fs.writeFileSync(reportPath, report);
    
    console.log(report);
    console.log(\`\\n📄 完整报告已保存到: \${reportPath}\\n\`);

    // 判断是否有阻断性问题
    const hasCriticalIssues = report.includes('## 🚨 Critical Issues') 
      && !report.match(/## 🚨 Critical Issues\\s*\\n\\s*无/);

    return {
      shouldBlock: hasCriticalIssues,
      report
    };

  } catch (error) {
    console.error('❌ AI 审查失败:', error.message);
    // 降级策略:AI 失败不阻断提交
    console.log('⚠️  AI 服务不可用,跳过审查(降级模式)');
    return { shouldBlock: false, report: '' };
  }
}

async function main() {
  const diff = getStagedDiff();
  const { shouldBlock, report } = await reviewCode(diff);

  if (shouldBlock) {
    console.error('\\n❌ 发现阻断性问题,请修复后再提交\\n');
    process.exit(1);
  } else {
    console.log('\\n✅ 代码审查通过\\n');
    process.exit(0);
  }
}

main();
3. 配置 Husky Hook

.husky/pre-commit

bash 复制代码
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# 运行 AI 代码审查
node scripts/ai-code-review.js

# 如果审查通过,继续执行 lint-staged
npx lint-staged
4. 配置环境变量
bash 复制代码
# .env
ANTHROPIC_API_KEY=your_api_key_here

真实案例:AI 发现的问题

上周我提交了一个表单组件,AI 审查发现了一个我完全没注意到的问题:

jsx 复制代码
// 我的原始代码
function SearchInput({ onSearch }) {
  const [query, setQuery] = useState('');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    onSearch(query);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      <button type="submit">搜索</button>
    </form>
  );
}

AI 的审查报告:

markdown 复制代码
## ⚠️ Warnings

- [SearchInput.jsx:7] **可访问性问题**:input 缺少 label 或 aria-label,
  屏幕阅读器用户无法理解这个输入框的用途。
  
  建议修复:
  <label htmlFor="search-input" className="sr-only">搜索</label>
  <input 
    id="search-input"
    aria-label="搜索内容"
    ...
  />

- [SearchInput.jsx:12] **用户体验**:提交空字符串会触发无意义的搜索。
  建议在 handleSubmit 中添加校验:
  if (query.trim().length === 0) return;

## 📊 Summary
- 总体评分:7/10
- 功能正常,但可访问性和边界情况处理需要改进
- 建议修复后合并

这是一个典型的"功能能跑,但不够专业"的例子。人工审查很可能会漏掉可访问性问题,但 AI 每次都会检查。

二、自动化测试用例生成

为什么测试覆盖率总是上不去?

老实说:写测试太枯燥了

你写完一个复杂组件,兴奋地想看效果,结果还要花同样的时间写一堆重复的测试用例。最后就变成了:

  • 核心逻辑有测试(因为有人盯着)
  • 边界情况?等有时间再说
  • UI 组件?手动点一下就行了
  • E2E 测试?那是 QA 的事

结果就是测试覆盖率 30%,然后生产环境各种边界 case 爆炸。

AI 生成测试的思路

AI 看代码比我们快,而且它知道所有的测试模式。我的做法是:

  1. 单元测试:根据组件/函数生成 Jest + React Testing Library 测试
  2. E2E 测试:根据用户流程生成 Playwright 脚本
  3. 重点覆盖边界情况:空数组、异常数据、网络失败等

实战:单元测试生成

创建脚本 scripts/generate-tests.js

javascript 复制代码
#!/usr/bin/env node
import Anthropic from '@anthropic-ai/sdk';
import fs from 'fs';
import path from 'path';

const client = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY
});

const TEST_GENERATION_PROMPT = \`你是一个测试工程师,擅长编写高质量的前端测试。

请为以下代码生成完整的测试用例,使用 Jest + React Testing Library。

**要求:**
1. 覆盖所有主要功能路径
2. 包含边界情况:空数组、null、undefined、错误数据
3. 测试用户交互:点击、输入、提交
4. 测试异步逻辑:API 调用成功和失败的情况
5. 测试可访问性:aria 属性、键盘导航
6. 使用语义化查询(getByRole > getByTestId)

**输出格式:**
- 完整可运行的测试文件
- 包含必要的 import
- 每个测试用例有清晰的描述
- 使用 describe 分组组织测试

现在请为以下代码生成测试:\`;

async function generateTestForFile(filePath) {
  const sourceCode = fs.readFileSync(filePath, 'utf-8');
  const fileName = path.basename(filePath, path.extname(filePath));

  console.log(\`🤖 正在为 \${fileName} 生成测试...\\n\`);

  try {
    const message = await client.messages.create({
      model: 'claude-sonnet-4-5',
      max_tokens: 8192,
      messages: [{
        role: 'user',
        content: \`\${TEST_GENERATION_PROMPT}\\n\\n\\\`\\\`\\\`jsx\\n\${sourceCode}\\n\\\`\\\`\\\`\`
      }]
    });

    const testCode = message.content[0].text
      .replace(/^\\\`\\\`\\\`[a-z]*\\n/, '')
      .replace(/\\n\\\`\\\`\\\`$/, '');

    // 确定输出路径
    const testFilePath = filePath.replace(/\\.(jsx?|tsx?)$/, '.test.$1');
    
    fs.writeFileSync(testFilePath, testCode);
    console.log(\`✅ 测试已生成: \${testFilePath}\\n\`);

    return testFilePath;

  } catch (error) {
    console.error(\`❌ 生成失败: \${error.message}\`);
    throw error;
  }
}

// 示例:为指定文件生成测试
const targetFile = process.argv[2];
if (!targetFile) {
  console.error('用法: node generate-tests.js <文件路径>');
  process.exit(1);
}

generateTestForFile(targetFile);

真实示例:AI 生成的测试

我有一个 UserProfile.jsx 组件:

jsx 复制代码
import { useState, useEffect } from 'react';

export function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        const res = await fetch(\`/api/users/\${userId}\`);
        if (!res.ok) throw new Error('加载失败');
        const data = await res.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }
    fetchUser();
  }, [userId]);

  if (loading) return <div role="status">加载中...</div>;
  if (error) return <div role="alert">{error}</div>;
  if (!user) return null;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

运行 node scripts/generate-tests.js src/components/UserProfile.jsx,AI 生成的测试:

javascript 复制代码
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';

// Mock fetch
global.fetch = jest.fn();

describe('UserProfile', () => {
  beforeEach(() => {
    fetch.mockClear();
  });

  describe('加载状态', () => {
    test('初始渲染时显示加载提示', () => {
      fetch.mockImplementation(() => new Promise(() => {})); // 永不 resolve
      render(<UserProfile userId="123" />);
      
      expect(screen.getByRole('status')).toHaveTextContent('加载中...');
    });
  });

  describe('成功场景', () => {
    test('成功获取用户数据后显示用户信息', async () => {
      const mockUser = {
        id: '123',
        name: '张三',
        email: 'zhang@example.com'
      };

      fetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockUser
      });

      render(<UserProfile userId="123" />);

      await waitFor(() => {
        expect(screen.getByText('张三')).toBeInTheDocument();
      });

      expect(screen.getByText('zhang@example.com')).toBeInTheDocument();
      expect(fetch).toHaveBeenCalledWith('/api/users/123');
    });
  });

  describe('错误处理', () => {
    test('API 返回错误时显示错误信息', async () => {
      fetch.mockResolvedValueOnce({
        ok: false
      });

      render(<UserProfile userId="123" />);

      await waitFor(() => {
        expect(screen.getByRole('alert')).toHaveTextContent('加载失败');
      });
    });

    test('网络请求失败时显示错误信息', async () => {
      fetch.mockRejectedValueOnce(new Error('网络错误'));

      render(<UserProfile userId="456" />);

      await waitFor(() => {
        expect(screen.getByRole('alert')).toHaveTextContent('网络错误');
      });
    });
  });

  describe('边界情况', () => {
    test('用户数据为 null 时不渲染任何内容', async () => {
      fetch.mockResolvedValueOnce({
        ok: true,
        json: async () => null
      });

      const { container } = render(<UserProfile userId="789" />);

      await waitFor(() => {
        expect(container.firstChild).toBeNull();
      });
    });

    test('userId 变化时重新获取数据', async () => {
      fetch.mockResolvedValue({
        ok: true,
        json: async () => ({ id: '1', name: 'User 1', email: 'user1@example.com' })
      });

      const { rerender } = render(<UserProfile userId="1" />);
      
      await waitFor(() => {
        expect(screen.getByText('User 1')).toBeInTheDocument();
      });

      fetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ id: '2', name: 'User 2', email: 'user2@example.com' })
      });

      rerender(<UserProfile userId="2" />);

      await waitFor(() => {
        expect(screen.getByText('User 2')).toBeInTheDocument();
      });

      expect(fetch).toHaveBeenCalledTimes(2);
    });
  });

  describe('可访问性', () => {
    test('加载状态使用正确的 ARIA role', () => {
      fetch.mockImplementation(() => new Promise(() => {}));
      render(<UserProfile userId="123" />);
      
      expect(screen.getByRole('status')).toBeInTheDocument();
    });

    test('错误信息使用正确的 ARIA role', async () => {
      fetch.mockRejectedValueOnce(new Error('测试错误'));
      render(<UserProfile userId="123" />);

      await waitFor(() => {
        expect(screen.getByRole('alert')).toBeInTheDocument();
      });
    });
  });
});

注意 AI 做了什么:

  1. ✅ 覆盖了加载、成功、错误、边界四种场景
  2. ✅ 测试了 userId 变化时的重新获取逻辑(这个我自己写可能会漏)
  3. ✅ 检查了可访问性(role="status" 和 role="alert")
  4. ✅ 所有测试用例都有清晰的描述
  5. ✅ 使用了语义化查询(getByRole > getByText)

这个测试我自己写至少要 30 分钟,AI 生成只要 10 秒,Token 成本 ¥0.15。

E2E 测试生成

对于端到端测试,我用类似的思路生成 Playwright 脚本。生成的测试示例:

javascript 复制代码
import { test, expect } from '@playwright/test';

test.describe('用户登录流程', () => {
  test('成功登录并跳转到首页', async ({ page }) => {
    await page.goto('/login');

    // 填写表单
    await page.getByLabel('用户名').fill('testuser');
    await page.getByLabel('密码').fill('password123');

    // 提交
    await page.getByRole('button', { name: '登录' }).click();

    // 等待跳转
    await expect(page).toHaveURL('/dashboard');

    // 验证用户信息
    await expect(page.getByText('欢迎, testuser')).toBeVisible();
  });

  test('用户名为空时显示错误', async ({ page }) => {
    await page.goto('/login');

    await page.getByLabel('密码').fill('password123');
    await page.getByRole('button', { name: '登录' }).click();

    await expect(page.getByText('请输入用户名')).toBeVisible();
    await expect(page).toHaveURL('/login'); // 未跳转
  });

  test('密码错误时显示提示', async ({ page }) => {
    await page.goto('/login');

    await page.getByLabel('用户名').fill('testuser');
    await page.getByLabel('密码').fill('wrongpassword');
    await page.getByRole('button', { name: '登录' }).click();

    await expect(page.getByRole('alert')).toHaveText('用户名或密码错误');
  });

  test('网络错误时显示友好提示', async ({ page }) => {
    // 模拟网络错误
    await page.route('/api/auth/login', route => route.abort());

    await page.goto('/login');
    await page.getByLabel('用户名').fill('testuser');
    await page.getByLabel('密码').fill('password123');
    await page.getByRole('button', { name: '登录' }).click();

    await expect(page.getByText('网络错误,请稍后重试')).toBeVisible();
  });
});

三、效果对比:数据说话

我在一个中型项目(15k 行代码,30+ 组件)上用了一个月 AI 代码审查 + 测试生成,数据如下:

测试覆盖率提升

指标 使用前 使用后 提升
单元测试覆盖率 32% 78% +144%
E2E 测试场景数 5 23 +360%
发现的边界 case bug - 17 个 -

成本分析

人工成本:

  • 代码审查:平均每个 PR 20 分钟 × 高级工程师时薪 ¥300/h = ¥100/PR
  • 写测试:平均每个组件 40 分钟 × ¥300/h = ¥200/组件

AI 成本:

  • 代码审查:平均 3k tokens 输入 + 1.5k tokens 输出 = ¥0.08/PR (Claude Sonnet 4.5)
  • 测试生成:平均 2k tokens 输入 + 4k tokens 输出 = ¥0.12/组件

一个月数据(50 个 PR,30 个组件):

  • 人工成本:¥5000 (审查) + ¥6000 (测试) = ¥11000
  • AI 成本:¥4 (审查) + ¥3.6 (测试) = ¥7.6
  • 成本降低:99.93%

时间节省:

  • 代码审查:从 20 分钟降到 2 分钟查看报告 = 节省 90%
  • 测试编写:从 40 分钟降到 5 分钟微调 AI 生成的测试 = 节省 87.5%

质量提升

AI 发现的问题类型分布:

  • 可访问性问题:47%(这个人工审查几乎不查)
  • 边界情况处理:28%
  • 性能优化建议:15%
  • 安全隐患:10%

最有价值的发现:一个组件在处理大数组(10k+ 项)时会卡死,AI 建议加虚拟滚动,修复后性能提升 95%。

四、注意事项

1. AI 不是银弹

  • 误报率:约 5%-10%,需要人工判断
  • 上下文限制:无法理解业务逻辑,只能检查技术层面
  • 降级策略:AI 服务挂了不能阻断开发流程

2. Prompt 很重要

我的 Prompt 迭代了 5 个版本才稳定。关键点:

  • ✅ 明确输出格式(Markdown、JSON)
  • ✅ 提供具体的检查项(别说"检查代码质量")
  • ✅ 给出示例(few-shot learning)
  • ✅ 设置边界(什么情况下 LGTM,什么情况下阻断)

3. 人机协作

最佳实践:

  • AI 做第一轮全面扫描
  • 人工审查 AI 标记的 Critical Issues
  • 对于复杂逻辑,还是需要资深工程师深度 Review

五、下一步:成本优化和团队协作

AI 代码审查和测试生成已经证明了价值,但还有两个问题需要解决:

  1. 成本优化:虽然单次成本低,但频繁调用还是会累积。能不能用更便宜的模型?
  2. 团队协作:如何让整个团队用上这套工具?如何统一标准?

下一篇文章我们会深入讨论:

  • 成本降低 80% 的 Prompt 缓存技巧
  • 多模型组合策略(简单检查用快速模型,复杂逻辑用强模型)
  • 团队级 AI Agent 工作流(从个人工具到团队基础设施)

敬请期待。


原文链接:AI Agent 前端工作流(二):智能代码审查与自动化测试

相关推荐
汽车软件工程师0013 小时前
ChatGpt指导嵌入式软件开发能力
人工智能·chatgpt·autosar
丝斯20113 小时前
AI学习笔记整理(70)——AI+CAE
人工智能·笔记·学习
Elastic 中国社区官方博客3 小时前
可执行操作的 AI agents:使用 Agent Builder 和 Workflows 自动化 IT 请求
大数据·数据库·人工智能·elasticsearch·搜索引擎·自动化·全文检索
电商API_180079052473 小时前
电商评论数据爬虫:情感分析与数据可视化实战
大数据·人工智能·爬虫·信息可视化·数据分析
凯子坚持 c3 小时前
Agentic AI最小可用部署方案:基于 SQLite + ChromaDB 构建 openJiuwen 本地轻量化智能体平台
数据库·人工智能·sqlite·openclaw
这是个栗子3 小时前
AI辅助编程工具(五) - Fitten Code
人工智能·ai编程·fitten code
子午3 小时前
【海洋生物识别系统】Python+深度学习+人工智能+算法模型+图像识别+tensoflow+2026计算机毕设项目
人工智能·python·深度学习
大模型实验室Lab4AI3 小时前
GDPO:多目标强化学习高效优化新路径
人工智能·深度学习·算法·机器学习
小刘的大模型笔记3 小时前
RAG技术实战
人工智能