1. 项目概述:告别“测试剧场”,让AI助手写出真正有用的测试

最近在几个项目里用Claude Code和Cursor的AI助手写单元测试,发现一个挺普遍的问题:它生成的测试代码看起来有模有样,覆盖率报告也漂漂亮亮,但真遇到边界情况或者逻辑错误时,这些测试却像个摆设一样毫无反应。后来在社区里看到有人把这种现象叫做“测试剧场”——测试只是在表演,并没有真正起到防护作用。

这个 anti-test-theater 项目就是专门为解决这个问题而生的。它是一个 agent-skills 技能包,安装后能让你的AI编程助手(包括Claude Code、Cursor、Kiro、GitHub Copilot等)学会如何编写高质量的、能真正捕获bug的测试代码。核心思路不是简单地禁止AI写测试,而是通过一套明确的规则和范例,引导AI避开常见的测试反模式,转向基于需求的、有实际价值的测试设计。

我在自己的TypeScript和Python项目里实际用了一段时间,最明显的感受是:测试代码的“智商”明显提高了。以前AI可能会生成那种把实现逻辑在测试里重写一遍的“同义反复”测试,现在它会主动考虑边界条件、异常场景和实际业务需求。对于每天要和测试代码打交道的开发者来说,这能省下大量手动修改和补充测试的时间。

2. “测试剧场”的五大反模式深度解析

2.1 实现逻辑镜像:最隐蔽的无效测试

这是AI生成测试时最容易掉进去的坑,而且乍一看还挺有迷惑性。所谓“实现逻辑镜像”,就是测试代码几乎原封不动地复制了被测试函数的内部逻辑。

// 被测试的函数
function calculateTotal(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

// AI可能生成的“测试剧场”版本
test('calculates total correctly', () => {
  const items = [{ price: 10, quantity: 2 }];
  // 注意这里:测试里重新实现了完全相同的reduce逻辑
  const expected = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  expect(calculateTotal(items)).toBe(expected);
});

这种测试的问题在于,它和实现形成了“循环论证”。如果 calculateTotal 的实现有bug——比如不小心把乘法写成了加法——那么测试里用相同错误逻辑计算出的 expected 值也会是错的,测试依然会通过。这种测试除了让覆盖率数字好看,没有任何实际价值。

正确的做法应该是基于需求来设计测试用例 ,而不是基于实现。对于上面的例子,我们应该思考:“这个函数在业务上应该满足什么要求?”然后针对这些要求设计测试:

test('计算包含多个商品的总价', () => {
  const items = [
    { price: 10, quantity: 2 },  // 20
    { price: 5, quantity: 3 },   // 15
    { price: 1, quantity: 10 }   // 10
  ];
  // 直接使用预期的业务结果,而不是重新计算
  expect(calculateTotal(items)).toBe(45);
});

test('空购物车返回0', () => {
  expect(calculateTotal([])).toBe(0);
});

test('正确处理浮点数精度问题', () => {
  const items = [
    { price: 0.1, quantity: 1 },
    { price: 0.2, quantity: 1 }
  ];
  // 使用toBeCloseTo而不是toBe,避免0.1+0.2≠0.3的经典问题
  expect(calculateTotal(items)).toBeCloseTo(0.3);
});

2.2 过度Mock:让测试失去意义的“皇帝的新衣”

Mock在单元测试中是必要的,但AI经常过度使用。根据项目文档中引用的数据,大约40%由AI生成的Mock对象本身就是有问题的。过度Mock会导致测试不再验证实际的集成行为,而是变成了“Mock配置是否正确”的测试。

// 过度Mock的典型例子
test('getUserData fetches user', async () => {
  // Mock了fetch函数
  global.fetch = jest.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ id: 1, name: 'Alice' })
  });
  
  const result = await getUserData(1);
  
  // 这里只是在验证Mock的配置,而不是实际的行为
  expect(global.fetch).toHaveBeenCalledWith('/api/users/1');
  expect(result).toEqual({ id: 1, name: 'Alice' });
});

这个测试的问题在于:如果 getUserData 函数内部把响应解析错了(比如尝试访问 data.user.name 但实际响应是 { name: 'Alice' } ),测试依然会通过,因为Mock返回的正是测试期望的结构。这完全失去了测试的意义。

Mock决策表是 anti-test-theater 提供的一个实用工具 ,它帮你判断什么时候该Mock,什么时候该用真实依赖:

依赖类型 Mock建议 理由 替代方案
外部API调用 推荐Mock 避免测试受网络影响,加速测试执行 使用MSW(Mock Service Worker)进行网络层拦截
数据库操作 视情况而定 单元测试Mock,集成测试用真实DB 使用内存数据库(SQLite)或测试容器
文件系统 通常Mock 避免测试产生副作用 使用内存文件系统或临时目录
当前模块的其他函数 不要Mock 应该测试完整的模块行为 如果必须Mock,考虑重构模块职责
第三方库 谨慎Mock 可能掩盖库的版本兼容问题 使用库提供的测试工具或适配器

对于上面的 getUserData 例子,更好的测试策略是:

// 方案1:使用MSW进行网络层Mock(更接近真实场景)
import { setupServer } from 'msw/node';
import { rest } from 'msw';

const server = setupServer(
  rest.get('/api/users/:id', (req, res, ctx) => {
    return res(ctx.json({ id: 1, name: 'Alice', email: 'alice@example.com' }));
  })
);

test('正确解析用户数据', async () => {
  const user = await getUserData(1);
  // 验证业务逻辑:我们需要name和email字段
  expect(user).toHaveProperty('name', 'Alice');
  expect(user).toHaveProperty('email', 'alice@example.com');
});

// 方案2:测试错误处理
test('处理404错误', async () => {
  server.use(
    rest.get('/api/users/:id', (req, res, ctx) => {
      return res(ctx.status(404));
    })
  );
  
  await expect(getUserData(999)).rejects.toThrow('User not found');
});

2.3 只测“快乐路径”:遗漏了真正的bug藏身地

AI生成的测试往往只覆盖最常规、最顺利的执行路径。但根据经验,80%的bug都发生在边界条件、异常情况和错误处理路径上。

// 只测快乐路径的例子
test('divides numbers correctly', () => {
  expect(divide(10, 2)).toBe(5);
  expect(divide(9, 3)).toBe(3);
});

这个测试漏掉了所有真正重要的情况:除数为0怎么办?负数怎么处理?浮点数除法呢?大数除法呢?

anti-test-theater 引导AI采用基于需求的四步测试设计法

  1. 识别需求 :这个函数要解决什么问题?业务规则是什么?
  2. 列出输入空间 :所有可能的输入组合,包括有效和无效的
  3. 定义边界 :数值边界、集合边界、状态边界
  4. 设计用例 :为每个重要场景设计具体的测试用例

对于除法函数,完整的测试应该包括:

describe('divide函数', () => {
  test('正常整数除法', () => {
    expect(divide(10, 2)).toBe(5);
    expect(divide(9, 3)).toBe(3);
  });
  
  test('浮点数除法', () => {
    expect(divide(1, 3)).toBeCloseTo(0.333333, 5);
  });
  
  test('除数为0时抛出错误', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });
  
  test('处理负数', () => {
    expect(divide(-10, 2)).toBe(-5);
    expect(divide(10, -2)).toBe(-5);
    expect(divide(-10, -2)).toBe(5);
  });
  
  test('被除数为0', () => {
    expect(divide(0, 5)).toBe(0);
  });
  
  test('大数处理', () => {
    expect(divide(Number.MAX_SAFE_INTEGER, 2)).toBe(Number.MAX_SAFE_INTEGER / 2);
  });
});

2.4 快照滥用:当toMatchSnapshot()成为万能胶

快照测试在UI组件测试中很有用,但AI容易把它当成“一键生成断言”的快捷方式,导致快照测试被滥用。

// 快照滥用的例子
test('UserProfile renders correctly', () => {
  const { container } = render(<UserProfile user={mockUser} />);
  expect(container).toMatchSnapshot();  // 过于笼统
});

这种测试的问题:

  1. 脆弱性高 :任何无关的样式改动都会导致快照失败
  2. 意图不明确 :我们到底在测试什么?是结构?内容?还是样式?
  3. 维护成本高 :每次UI调整都要更新快照,容易产生“快照疲劳”

正确的快照使用策略应该是针对性的

test('显示用户基本信息', () => {
  const user = { name: 'Alice', email: 'alice@test.com', role: 'admin' };
  render(<UserProfile user={user} />);
  
  // 针对性地测试关键内容
  expect(screen.getByText('Alice')).toBeInTheDocument();
  expect(screen.getByText('alice@test.com')).toBeInTheDocument();
  expect(screen.getByText('Admin')).toBeInTheDocument();
});

test('管理员看到管理按钮', () => {
  const user = { role: 'admin' };
  render(<UserProfile user={user} />);
  expect(screen.getByRole('button', { name: /manage/i })).toBeInTheDocument();
});

test('普通用户看不到管理按钮', () => {
  const user = { role: 'user' };
  render(<UserProfile user={user} />);
  expect(screen.queryByRole('button', { name: /manage/i })).not.toBeInTheDocument();
});

// 如果确实需要用快照,也应该是有针对性的
test('组件结构稳定', () => {
  const { container } = render(<UserProfile user={mockUser} />);
  // 只快照关键的结构部分,忽略易变的样式类名
  const structure = container.querySelector('.user-profile');
  expect(structure).toMatchSnapshot();
});

2.5 脆弱的异步测试:setTimeout(2000)和祈祷

异步测试是另一个重灾区。项目文档提到大约30%的AI生成的异步测试是脆弱的(flaky)。脆弱的测试有时通过有时失败,这种测试比没有测试更糟糕——它消耗信任,让人养成忽略测试失败的习惯。

// 脆弱的异步测试
test('loads data and updates state', async () => {
  fetchData();  // 触发异步操作
  await setTimeout(2000);  // 祈祷2秒足够
  expect(store.getState().data).toEqual(expectedData);
});

这种测试的问题很明显:如果网络慢一点怎么办?如果后端处理需要2.1秒怎么办?如果测试环境负载高怎么办?

稳健的异步测试应该基于状态或事件 ,而不是基于时间:

// 方案1:等待特定的UI状态
test('数据加载后显示内容', async () => {
  render(<DataComponent />);
  
  // 初始应该是加载状态
  expect(screen.getByText('Loading...')).toBeInTheDocument();
  
  // 等待数据加载完成
  expect(await screen.findByText('Data loaded')).toBeInTheDocument();
  
  // 验证具体内容
  expect(screen.getByText('Item 1')).toBeInTheDocument();
});

// 方案2:使用Mock控制异步时序
test('处理加载错误', async () => {
  // Mock一个会失败的fetch
  jest.spyOn(api, 'fetchData').mockRejectedValue(new Error('Network error'));
  
  render(<DataComponent />);
  
  // 应该显示错误信息
  expect(await screen.findByText(/failed to load/i)).toBeInTheDocument();
  
  // 验证重试按钮存在
  expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument();
});

// 方案3:测试异步操作的状态变化
test('跟踪异步操作状态', async () => {
  const { result } = renderHook(() => useAsyncData());
  
  // 初始状态
  expect(result.current.isLoading).toBe(false);
  expect(result.current.data).toBeNull();
  
  // 触发加载
  act(() => {
    result.current.loadData();
  });
  
  // 应该进入加载状态
  expect(result.current.isLoading).toBe(true);
  
  // 等待加载完成
  await waitFor(() => {
    expect(result.current.isLoading).toBe(false);
  });
  
  // 验证数据
  expect(result.current.data).toEqual(expect.any(Array));
});

3. 实战:将anti-test-theater集成到开发工作流

3.1 安装与配置详解

安装 anti-test-theater 非常简单,但它支持多种AI开发工具,配置方式略有不同。我以最常用的几个工具为例说明:

# 基础安装(适用于大多数基于Claude Code的工具链)
npx skills add nanami7777777/anti-test-theater

# 如果你使用特定的AI开发环境
# 对于Cursor用户:安装后会在编写测试时自动应用规则
# 对于GitHub Copilot:需要在VSCode设置中启用技能集成
# 对于Aider:在.aider.conf.yml中添加技能配置

安装完成后,你可以在 ~/.claude/skills/anti-test-theater/ 目录下找到所有资源文件。这个目录结构设计得很实用:

~/.claude/skills/anti-test-theater/
├── SKILL.md                    # 核心规则文件(AI会自动加载)
├── reference/                  # 各技术栈的详细参考
│   ├── frontend-testing.md     # React/Vue/Playwright最佳实践
│   ├── api-testing.md          # API测试模式(含并发测试)
│   ├── go-testing.md           # Go语言的表格驱动测试
│   ├── java-testing.md         # JUnit 5和Mockito模式
│   └── csharp-testing.md       # .NET测试策略
└── scripts/
    └── check-test-quality.sh   # 测试质量检查脚本

配置技巧 :我发现在项目根目录创建一个 .clauderc 文件可以进一步定制AI的行为:

{
  "testing": {
    "preferIntegrationOverUnit": true,
    "requireEdgeCases": true,
    "maxMockDepth": 2,
    "testNamingConvention": "should_$behavior_when_$condition"
  }
}

3.2 测试质量检查脚本的深度使用

项目提供的 check-test-quality.sh 脚本是个宝藏工具,但很多人只是简单地运行它。其实它可以做很多定制化的检查:

# 基本用法:扫描整个src目录
bash ~/.claude/skills/anti-test-theater/scripts/check-test-quality.sh src/

# 只检查特定的测试文件
bash ~/.claude/skills/anti-test-theater/scripts/check-test-quality.sh src/components/__tests__/

# 生成详细的HTML报告(需要安装jq)
bash ~/.claude/skills/anti-test-theater/scripts/check-test-quality.sh src/ --format html > test-quality-report.html

# 集成到CI/CD流水线中,设置质量阈值
bash ~/.claude/skills/anti-test-theater/scripts/check-test-quality.sh src/ --min-score 80

这个脚本会检查以下反模式:

  1. 同义反复测试 :测试中重新实现了业务逻辑
  2. 过度Mock :Mock了不应该Mock的依赖
  3. 缺少边界测试 :只有正常路径的测试
  4. 脆弱的异步 :使用固定时间等待
  5. 模糊的快照 :对整个组件使用快照测试

我在团队中配置了Git预提交钩子,在提交前自动运行这个检查:

#!/bin/bash
# .git/hooks/pre-commit

# 运行测试质量检查
TEST_QUALITY_REPORT=$(bash ~/.claude/skills/anti-test-theater/scripts/check-test-quality.sh src/ --format json)

# 解析JSON输出,检查分数
SCORE=$(echo "$TEST_QUALITY_REPORT" | jq '.score')
if [ "$SCORE" -lt 70 ]; then
  echo "❌ 测试质量分数过低: $SCORE/100"
  echo "请修复以下问题:"
  echo "$TEST_QUALITY_REPORT" | jq -r '.issues[] | "- \(.file):\(.line) \(.type): \(.message)"'
  exit 1
fi

3.3 不同技术栈的适配策略

anti-test-theater 的reference目录包含了针对不同技术栈的详细指南,但你需要知道如何有效地利用它们。

对于前端项目(React/Vue)

// 参考frontend-testing.md的最佳实践
// 1. 测试用户交互,而不是实现细节
test('点击按钮提交表单', async () => {
  render(<ContactForm />);
  const submitButton = screen.getByRole('button', { name: /submit/i });
  
  // 错误的做法:测试内部状态
  // expect(formInstance.state.isSubmitting).toBe(true);
  
  // 正确的做法:测试用户可见的行为
  fireEvent.click(submitButton);
  expect(await screen.findByText('Submitting...')).toBeInTheDocument();
});

// 2. 使用Testing Library的查询优先级
// 从用户角度查询,而不是实现角度
const goodQueries = [
  'getByRole',      // 最推荐:按角色查询(按钮、链接等)
  'getByLabelText', // 表单标签
  'getByPlaceholderText', // 占位符
  'getByText',      // 文本内容
  'getByDisplayValue', // 显示的值
];

const badQueries = [
  'getByTestId',    // 最后的选择:测试ID
  'container.querySelector', // 避免:太依赖实现
];

对于API测试

# 参考api-testing.md的Python示例
# 1. 测试完整的请求-响应周期
def test_create_user_validation():
    # 错误的做法:只测试序列化器
    # serializer = UserSerializer(data={'email': 'invalid'})
    # assert not serializer.is_valid()
    
    # 正确的做法:测试完整的API端点
    response = client.post('/api/users/', {'email': 'invalid'})
    assert response.status_code == 400
    assert 'email' in response.json()

# 2. 并发测试(经常被忽略)
def test_concurrent_user_creation():
    import threading
    
    def create_user():
        response = client.post('/api/users/', {'email': f'test{threading.get_ident()}@test.com'})
        return response.status_code
    
    # 模拟并发创建
    threads = [threading.Thread(target=create_user) for _ in range(10)]
    [t.start() for t in threads]
    [t.join() for t in threads]
    
    # 验证没有重复用户被创建
    user_count = User.objects.filter(email__startswith='test').count()
    assert user_count <= 10  # 应该正好是10,但<=10可以检测重复问题

对于Go项目

// 参考go-testing.md的表格驱动测试模式
func TestDivide(t *testing.T) {
    tests := []struct {
        name        string
        a, b        float64
        want        float64
        expectError bool
    }{
        {
            name:   "正常除法",
            a:      10,
            b:      2,
            want:   5,
        },
        {
            name:        "除数为零",
            a:           10,
            b:           0,
            expectError: true,
        },
        {
            name: "浮点数除法",
            a:    1,
            b:    3,
            want: 0.3333333333333333,
        },
        {
            name: "负数除法",
            a:    -10,
            b:    2,
            want: -5,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Divide(tt.a, tt.b)
            
            if tt.expectError {
                if err == nil {
                    t.Errorf("Divide(%v, %v) 期望错误,但得到了 %v", tt.a, tt.b, got)
                }
                return
            }
            
            if err != nil {
                t.Errorf("Divide(%v, %v) 发生意外错误: %v", tt.a, tt.b, err)
                return
            }
            
            if math.Abs(got-tt.want) > 1e-9 {
                t.Errorf("Divide(%v, %v) = %v, 期望 %v", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

4. 高级技巧:让AI写出生产级测试代码

4.1 测试命名规范的实际应用

好的测试名应该让人一眼就知道它在测试什么、在什么条件下测试、期望什么结果。 anti-test-theater 推荐了一套命名规范,但需要根据实际情况调整:

// 不好的命名
test('testCalculateTotal', () => {})  // 冗余的"test"前缀
test('calculateTotal', () => {})      // 只是重复函数名
test('works', () => {})               // 太模糊

// 好的命名模式
describe('calculateTotal函数', () => {
  // 模式1:should_$behavior_when_$condition
  test('应该返回0当购物车为空时', () => {
    expect(calculateTotal([])).toBe(0);
  });
  
  // 模式2:given_$condition_when_$action_then_$result
  test('给定多个商品时当计算总价那么返回正确总和', () => {
    const items = [{ price: 10, qty: 2 }, { price: 5, qty: 3 }];
    expect(calculateTotal(items)).toBe(35);
  });
  
  // 模式3:针对特定场景
  test('处理浮点数精度问题', () => {
    const items = [{ price: 0.1, qty: 1 }, { price: 0.2, qty: 1 }];
    expect(calculateTotal(items)).toBeCloseTo(0.3);
  });
  
  // 模式4:错误场景
  test('商品数量为负数时抛出错误', () => {
    const items = [{ price: 10, qty: -1 }];
    expect(() => calculateTotal(items)).toThrow('数量不能为负数');
  });
});

在实际项目中,我建议团队统一使用一种命名模式。我个人偏好第一种(should_$behavior_when_$condition),因为它读起来最自然,也最容易让AI理解。

4.2 测试粒度的智能选择

什么时候写单元测试?什么时候写集成测试?什么时候写E2E测试?AI往往分不清这些界限。 anti-test-theater 提供了一个实用的决策框架:

测试类型 适合场景 执行速度 可靠性 维护成本 AI生成建议
单元测试 纯函数、工具函数、简单组件 快(<100ms) 推荐AI生成
集成测试 模块间交互、API端点、数据库操作 中(100ms-2s) 部分AI生成,需人工审查
E2E测试 完整用户流程、跨系统交互 慢(>2s) 不推荐AI生成

具体决策流程

  1. 如果是纯函数 (相同输入总是得到相同输出,无副作用)→ 单元测试
  2. 如果涉及外部依赖 (数据库、API、文件系统)→ 集成测试
  3. 如果涉及多个系统或完整用户流程 → E2E测试
  4. 如果测试需要浏览器环境 → 组件测试或E2E测试
// 单元测试示例:纯函数
describe('formatDate函数', () => {
  test('应该将ISO字符串格式化为本地日期', () => {
    expect(formatDate('2024-01-15T10:30:00Z')).toBe('2024年1月15日');
  });
});

// 集成测试示例:涉及数据库
describe('UserService', () => {
  let db: TestDatabase;
  
  beforeEach(async () => {
    db = await createTestDatabase();  // 使用测试数据库
  });
  
  test('创建用户并保存到数据库', async () => {
    const service = new UserService(db);
    const user = await service.createUser({ name: 'Alice', email: 'alice@test.com' });
    
    // 验证数据库中的实际数据
    const dbUser = await db.users.findOne({ id: user.id });
    expect(dbUser).toEqual(expect.objectContaining({
      name: 'Alice',
      email: 'alice@test.com'
    }));
  });
});

// E2E测试示例:完整流程(通常手动编写)
describe('用户注册流程', () => {
  test('新用户可以完成注册并登录', async () => {
    // 1. 访问注册页面
    await page.goto('https://app.example.com/register');
    
    // 2. 填写注册表单
    await page.fill('#email', 'newuser@example.com');
    await page.fill('#password', 'SecurePass123!');
    await page.click('button[type="submit"]');
    
    // 3. 验证注册成功
    await expect(page).toHaveURL(/dashboard/);
    await expect(page.locator('.welcome-message')).toContainText('欢迎');
  });
});

4.3 测试数据管理的艺术

AI生成的测试经常硬编码测试数据,这会导致两个问题:1) 测试数据与生产数据差异太大;2) 测试数据难以维护。

// 不好的做法:硬编码所有数据
test('计算订单总价', () => {
  const order = {
    id: 1,
    items: [
      { id: 101, name: '商品A', price: 100, quantity: 2 },
      { id: 102, name: '商品B', price: 50, quantity: 3 },
    ],
    customer: { id: 1001, name: '张三' },
    // ... 更多硬编码字段
  };
  
  expect(calculateOrderTotal(order)).toBe(350);
});

// 好的做法:使用工厂函数和构建器模式
import { OrderBuilder } from '../test/builders/OrderBuilder';

test('计算订单总价', () => {
  // 使用构建器创建测试订单
  const order = new OrderBuilder()
    .withItem({ price: 100, quantity: 2 })
    .withItem({ price: 50, quantity: 3 })
    .build();
  
  expect(calculateOrderTotal(order)).toBe(350);
});

// 更好的做法:分离测试数据和测试逻辑
describe('订单计算', () => {
  const testCases = [
    {
      name: '多个商品',
      items: [{ price: 100, qty: 2 }, { price: 50, qty: 3 }],
      expected: 350,
    },
    {
      name: '空订单',
      items: [],
      expected: 0,
    },
    {
      name: '折扣商品',
      items: [{ price: 100, qty: 1, discount: 20 }],
      expected: 80,
    },
  ];
  
  testCases.forEach(({ name, items, expected }) => {
    test(name, () => {
      const order = new OrderBuilder().withItems(items).build();
      expect(calculateOrderTotal(order)).toBe(expected);
    });
  });
});

测试数据工厂的实现示例

// test/factories/UserFactory.ts
export class UserFactory {
  static create(overrides: Partial<User> = {}): User {
    const defaults: User = {
      id: faker.datatype.uuid(),
      name: faker.name.fullName(),
      email: faker.internet.email(),
      age: faker.datatype.number({ min: 18, max: 65 }),
      createdAt: new Date(),
      isActive: true,
    };
    
    return { ...defaults, ...overrides };
  }
  
  static createList(count: number, overrides: Partial<User> = {}): User[] {
    return Array.from({ length: count }, () => this.create(overrides));
  }
  
  static createInactive(): User {
    return this.create({ isActive: false });
  }
  
  static createUnderage(): User {
    return this.create({ age: faker.datatype.number({ min: 13, max: 17 }) });
  }
}

// 在测试中使用
test('只返回活跃用户', () => {
  const users = [
    UserFactory.create({ isActive: true }),
    UserFactory.create({ isActive: false }),  // 应该被过滤掉
    UserFactory.create({ isActive: true }),
  ];
  
  const activeUsers = filterActiveUsers(users);
  expect(activeUsers).toHaveLength(2);
  expect(activeUsers.every(u => u.isActive)).toBe(true);
});

4.4 性能测试与基准测试

大多数AI生成的测试只关注正确性,忽略了性能。但在实际项目中,性能回归是常见问题。

// 性能测试示例
describe('searchProducts性能', () => {
  const largeProductList = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `产品${i}`,
    category: i % 10 === 0 ? '特价' : '常规',
    price: 10 + (i % 100),
  }));
  
  test('搜索响应时间应在100ms内', () => {
    const start = performance.now();
    const results = searchProducts(largeProductList, '特价');
    const duration = performance.now() - start;
    
    expect(duration).toBeLessThan(100);
    expect(results).toHaveLength(1000); // 10000个产品中10%是特价
  });
  
  test('内存使用不应线性增长', () => {
    const initialMemory = process.memoryUsage().heapUsed;
    
    // 多次搜索,内存使用应该稳定
    for (let i = 0; i < 100; i++) {
      searchProducts(largeProductList, `产品${i}`);
    }
    
    const finalMemory = process.memoryUsage().heapUsed;
    const memoryIncrease = finalMemory - initialMemory;
    
    // 内存增长应该小于1MB
    expect(memoryIncrease).toBeLessThan(1024 * 1024);
  });
});

// 基准测试(使用Benchmark.js)
import Benchmark from 'benchmark';

describe('排序算法性能比较', () => {
  test('快速排序 vs 归并排序', () => {
    const suite = new Benchmark.Suite();
    const testArray = Array.from({ length: 10000 }, () => Math.random());
    
    suite
      .add('快速排序', () => {
        quickSort([...testArray]);
      })
      .add('归并排序', () => {
        mergeSort([...testArray]);
      })
      .on('cycle', (event: any) => {
        console.log(String(event.target));
      })
      .on('complete', function (this: any) {
        console.log('最快的算法是: ' + this.filter('fastest').map('name'));
      })
      .run({ async: false });
  });
});

5. 常见问题排查与实战经验

5.1 AI生成的测试过于简单怎么办?

这是最常见的问题。AI倾向于生成最简化的测试用例,忽略了边界情况和错误处理。

解决方案 :在提示词中明确要求具体的测试场景:

请为calculateTotal函数编写完整的测试套件,包括:
1. 正常情况:多个商品的计算
2. 边界情况:空数组、单个商品、数量为0的商品
3. 错误情况:价格或数量为负数、非数字输入
4. 特殊数值:浮点数精度问题、极大数值
5. 性能考虑:大量商品时的计算效率

实际案例 :我曾经让AI测试一个价格计算函数,它最初只生成了两个测试用例。我按照上面的模板重新提示后,它生成了12个测试用例,包括了我没想到的“商品数量为小数”的情况。

5.2 测试与实现耦合太紧怎么办?

AI有时会生成过度依赖实现细节的测试,比如测试私有方法、测试内部状态变化。

// 不好的测试:测试实现细节
test('内部状态更新', () => {
  const instance = new ShoppingCart();
  instance.addItem({ id: 1, price: 10 });  // 公有方法
  expect(instance.items.length).toBe(1);    // 测试内部状态
  expect(instance.items[0].id).toBe(1);     // 测试内部数据结构
});

// 好的测试:测试公有行为
test('添加商品后可以获取总价', () => {
  const cart = new ShoppingCart();
  cart.addItem({ id: 1, price: 10, quantity: 2 });
  expect(cart.getTotal()).toBe(20);  // 只测试公有接口
});

test('添加重复商品时合并数量', () => {
  const cart = new ShoppingCart();
  cart.addItem({ id: 1, price: 10, quantity: 1 });
  cart.addItem({ id: 1, price: 10, quantity: 2 });
  expect(cart.getTotal()).toBe(30);  // 数量合并为3
});

经验法则 :只测试类的公有接口。如果发现需要测试私有方法,通常意味着这个类需要重构——要么将私有方法提取为公有工具函数,要么将类拆分成更小的单元。

5.3 异步测试难以稳定运行怎么办?

脆弱的异步测试是团队的一大痛点。除了前面提到的避免固定超时的方法,还有一些高级技巧:

// 技巧1:使用可配置的超时和重试
const withRetry = async <T>(
  fn: () => Promise<T>,
  options = { retries: 3, delay: 100 }
): Promise<T> => {
  let lastError: Error;
  
  for (let i = 0; i < options.retries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      if (i < options.retries - 1) {
        await new Promise(resolve => setTimeout(resolve, options.delay));
      }
    }
  }
  
  throw lastError;
};

test('不稳定的API调用', async () => {
  await withRetry(async () => {
    const result = await fetchUnstableApi();
    expect(result.status).toBe('success');
  }, { retries: 5, delay: 200 });
});

// 技巧2:使用TestContainers进行集成测试
import { GenericContainer, StartedTestContainer } from 'testcontainers';

describe('数据库集成测试', () => {
  let container: StartedTestContainer;
  let dbClient: DbClient;
  
  beforeAll(async () => {
    // 启动一个真实的PostgreSQL容器
    container = await new GenericContainer('postgres:15')
      .withExposedPorts(5432)
      .withEnvironment({
        POSTGRES_USER: 'test',
        POSTGRES_PASSWORD: 'test',
        POSTGRES_DB: 'test',
      })
      .start();
    
    const port = container.getMappedPort(5432);
    dbClient = new DbClient({
      host: 'localhost',
      port,
      user: 'test',
      password: 'test',
      database: 'test',
    });
  }, 30000); // 设置更长的超时
  
  afterAll(async () => {
    await dbClient.close();
    await container.stop();
  });
  
  test('真实的数据库操作', async () => {
    await dbClient.query('INSERT INTO users (name) VALUES ($1)', ['Alice']);
    const result = await dbClient.query('SELECT * FROM users');
    expect(result.rows).toHaveLength(1);
    expect(result.rows[0].name).toBe('Alice');
  });
});

5.4 测试维护成本太高怎么办?

随着项目演进,测试也需要维护。AI可以帮助减少维护成本:

// 使用参数化测试减少重复
describe.each([
  { input: [], expected: 0, description: '空数组' },
  { input: [{ price: 10, qty: 1 }], expected: 10, description: '单个商品' },
  { input: [{ price: 10, qty: 2 }, { price: 5, qty: 3 }], expected: 35, description: '多个商品' },
  { input: [{ price: 0.1, qty: 1 }, { price: 0.2, qty: 1 }], expected: 0.3, description: '浮点数' },
])('calculateTotal - $description', ({ input, expected }) => {
  test(`返回${expected}`, () => {
    expect(calculateTotal(input)).toBeCloseTo(expected);
  });
});

// 使用自定义匹配器提高可读性
expect.extend({
  toBeValidEmail(received) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const pass = emailRegex.test(received);
    
    return {
      message: () => `期望${received}是一个有效的邮箱地址`,
      pass,
    };
  },
  
  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    
    return {
      message: () => `期望${received}在${floor}和${ceiling}之间`,
      pass,
    };
  },
});

// 在测试中使用
test('验证用户邮箱', () => {
  const user = { email: 'user@example.com' };
  expect(user.email).toBeValidEmail();
});

test('年龄在有效范围内', () => {
  const user = { age: 25 };
  expect(user.age).toBeWithinRange(18, 100);
});

5.5 测试覆盖率高但质量低怎么办?

这是“测试剧场”的典型症状:覆盖率报告显示90%+,但bug依然频发。

诊断方法

  1. 检查行覆盖率 vs 分支覆盖率 :行覆盖率90%但分支覆盖率只有50%,说明很多条件分支没测试
  2. 检查突变测试结果 :使用Stryker等突变测试工具,看测试能否杀死突变体
  3. 手动代码审查 :重点审查复杂逻辑、边界条件、错误处理

改进策略

// 使用突变测试工具
// 安装:npm install --save-dev @stryker-mutator/core
// 配置:stryker.config.json
{
  "mutate": ["src/**/*.ts", "!src/**/*.test.ts"],
  "testRunner": "jest",
  "coverageAnalysis": "perTest"
}

// 运行突变测试后,查看哪些突变体没被杀死
// 这能揭示测试的薄弱环节

// 针对性地补充测试
describe('复杂的条件逻辑', () => {
  // 原始函数
  function calculateDiscount(price: number, userType: string, isPremium: boolean): number {
    if (price > 1000) {
      if (userType === 'vip') {
        return isPremium ? 0.2 : 0.15;
      } else {
        return 0.1;
      }
    } else if (price > 500) {
      return 0.05;
    } else {
      return 0;
    }
  }
  
  // 测试所有分支
  test.each([
    { price: 1200, userType: 'vip', isPremium: true, expected: 0.2 },
    { price: 1200, userType: 'vip', isPremium: false, expected: 0.15 },
    { price: 1200, userType: 'regular', isPremium: true, expected: 0.1 },
    { price: 1200, userType: 'regular', isPremium: false, expected: 0.1 },
    { price: 600, userType: 'vip', isPremium: true, expected: 0.05 },
    { price: 600, userType: 'regular', isPremium: false, expected: 0.05 },
    { price: 300, userType: 'vip', isPremium: true, expected: 0 },
    { price: 300, userType: 'regular', isPremium: false, expected: 0 },
  ])('价格$price, 用户$userType, 高级$isPremium => 折扣$expected', ({ price, userType, isPremium, expected }) => {
    expect(calculateDiscount(price, userType, isPremium)).toBe(expected);
  });
});

5.6 测试代码本身难以维护怎么办?

测试代码也是代码,也需要遵循良好的编码实践。

测试代码的SOLID原则

  • 单一职责 :每个测试只测一件事
  • 开放封闭 :易于添加新测试,无需修改现有测试
  • 里氏替换 :测试基类的地方,子类测试也能通过
  • 接口隔离 :测试只依赖它们需要的部分
  • 依赖倒置 :依赖注入,便于Mock
// 不好的测试:一个测试做多件事
test('用户注册流程', () => {
  // 测试验证逻辑
  expect(validateEmail('invalid')).toBe(false);
  expect(validateEmail('test@example.com')).toBe(true);
  
  // 测试数据库操作
  const user = createUser({ email: 'test@example.com' });
  expect(user.id).toBeDefined();
  
  // 测试邮件发送
  expect(sendWelcomeEmail).toHaveBeenCalledWith('test@example.com');
  
  // 太多断言,一个失败整个测试失败
});

// 好的测试:拆分关注点
describe('用户注册', () => {
  describe('邮箱验证', () => {
    test('无效邮箱返回false', () => {
      expect(validateEmail('invalid')).toBe(false);
    });
    
    test('有效邮箱返回true', () => {
      expect(validateEmail('test@example.com')).toBe(true);
    });
  });
  
  describe('用户创建', () => {
    test('创建用户并返回ID', () => {
      const user = createUser({ email: 'test@example.com' });
      expect(user.id).toBeDefined();
    });
  });
  
  describe('欢迎邮件', () => {
    test('新用户注册后发送欢迎邮件', () => {
      createUser({ email: 'test@example.com' });
      expect(sendWelcomeEmail).toHaveBeenCalledWith('test@example.com');
    });
  });
});

// 使用测试夹具减少重复
describe('购物车', () => {
  let cart: ShoppingCart;
  let mockProduct: Product;
  
  beforeEach(() => {
    // 每个测试前重新初始化
    cart = new ShoppingCart();
    mockProduct = {
      id: 'prod-1',
      name: '测试商品',
      price: 100,
      stock: 10,
    };
  });
  
  test('添加商品', () => {
    cart.addItem(mockProduct, 2);
    expect(cart.getTotal()).toBe(200);
  });
  
  test('移除商品', () => {
    cart.addItem(mockProduct, 2);
    cart.removeItem('prod-1');
    expect(cart.getTotal()).toBe(0);
  });
  
  // 每个测试都是独立的,不会相互影响
});

6. 测试策略与团队协作

6.1 制定团队的测试规范

引入 anti-test-theater 后,最好制定团队的测试编写规范。这里是我在团队中推行的一套规范:

# 测试编写规范

## 1. 测试结构
- 使用 `describe` 组织相关测试
- 每个 `describe` 对应一个被测单元(函数、类、模块)
- 使用 `beforeEach` 而不是 `beforeAll`,确保测试隔离
- 测试名使用 `should_$behavior_when_$condition` 格式

## 2. 测试内容
- 每个测试只验证一个行为
- 包含正常路径、边界情况和错误处理
- 避免测试实现细节,只测试公有接口
- 使用真实数据,避免魔法数字

## 3. Mock策略
- 只Mock外部依赖(API、数据库、文件系统)
- 使用MSW Mock网络请求,而不是直接Mock fetch
- 对于数据库,使用测试数据库或内存数据库
- 避免过度Mock,保持测试的真实性

## 4. 异步测试
- 使用 `findBy*` 查询等待元素出现
- 避免固定时间的 `setTimeout`
- 使用 `waitFor` 等待条件满足
- 设置合理的超时时间

## 5. 测试数据
- 使用工厂函数创建测试数据
- 对于复杂对象,使用构建器模式
- 将测试数据定义在测试文件顶部或单独的文件中
- 避免硬编码,使用有意义的变量名

## 6. 断言
- 每个测试至少有一个断言
- 使用具体的断言,避免模糊的 `toBeTruthy`
- 对于数组或对象,使用 `toHaveLength`、`toContain`、`toMatchObject`
- 错误断言使用 `toThrow` 和 `toThrowError`

## 7. 覆盖率目标
- 行覆盖率:≥80%
- 分支覆盖率:≥70%
- 函数覆盖率:≥90%
- 突变测试得分:≥80%

6.2 在CI/CD中集成质量检查

将测试质量检查集成到CI/CD流水线中,可以防止低质量测试进入代码库:

# .github/workflows/test-quality.yml
name: Test Quality Check

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

jobs:
  test-quality:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        
    - name: Install dependencies
      run: npm ci
      
    - name: Install anti-test-theater
      run: npx skills add nanami7777777/anti-test-theater
      
    - name: Run test quality check
      run: |
        bash ~/.claude/skills/anti-test-theater/scripts/check-test-quality.sh src/ \
          --min-score 75 \
          --format json \
          --output test-quality-report.json
          
    - name: Upload test quality report
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: test-quality-report
        path: test-quality-report.json
        
    - name: Comment on PR
      if: github.event_name == 'pull_request'
      uses: actions/github-script@v6
      with:
        script: |
          const report = require('./test-quality-report.json');
          const { score, issues } = report;
          
          let comment = `## 测试质量报告\n\n`;
          comment += `**得分**: ${score}/100\n\n`;
          
          if (score < 75) {
            comment += `❌ **未通过质量检查** (最低要求: 75)\n\n`;
          } else {
            comment += `✅ **通过质量检查**\n\n`;
          }
          
          if (issues.length > 0) {
            comment += `### 发现的问题:\n\n`;
            issues.forEach(issue => {
              comment += `- **${issue.file}:${issue.line}** - ${issue.type}: ${issue.message}\n`;
            });
          }
          
          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: comment
          });

6.3 测试代码审查清单

在代码审查时,除了业务逻辑,也要审查测试代码的质量。这是我们的测试代码审查清单:

## 测试代码审查清单

### 1. 测试设计
- [ ] 测试是否覆盖了正常路径?
- [ ] 测试是否覆盖了边界情况?
- [ ] 测试是否覆盖了错误处理?
- [ ] 测试是否避免了实现细节?
- [ ] 测试是否独立(不依赖其他测试)?

### 2. 测试可读性
- [ ] 测试名是否清晰描述了测试内容?
- [ ] 测试数据是否易于理解?
- [ ] 断言是否明确表达了期望?
- [ ] 是否有过多的设置代码?
- [ ] 是否使用了适当的帮助函数?

### 3. 测试可靠性
- [ ] 测试是否稳定(不脆弱)?
- [ ] 异步测试是否有适当的等待机制?
- [ ] 是否避免了固定时间的等待?
- [ ] Mock是否适当(不过度也不过少)?
- [ ] 测试是否清理了资源?

### 4. 测试维护性
- [ ] 测试是否遵循DRY原则?
- [ ] 是否有重复的测试代码?
- [ ] 测试数据是否易于修改?
- [ ] 测试是否易于理解?
- [ ] 测试失败时的错误信息是否有帮助?

### 5. 测试性能
- [ ] 测试运行速度是否合理?
- [ ] 是否有不必要的慢操作?
- [ ] 是否使用了适当的测试替身?
- [ ] 测试是否并行安全?

6.4 测试代码的重构策略

当测试代码变得难以维护时,需要重构。以下是一些常见的测试代码坏味道和重构方法:

// 坏味道1:重复的设置代码
// 重构前
test('测试A', () => {
  const db = createTestDatabase();
  const service = new UserService(db);
  const repo = new UserRepository(db);
  // ... 20行设置代码
  // 实际测试只有2行
});

test('测试B', () => {
  const db = createTestDatabase();
  const service = new UserService(db);
  const repo = new UserRepository(db);
  // ... 相同的20行设置代码
  // 另一个测试
});

// 重构后:使用beforeEach和工厂函数
describe('UserService', () => {
  let db: TestDatabase;
  let service: UserService;
  let repo: UserRepository;
  
  beforeEach(() => {
    db = createTestDatabase();
    service = new UserService(db);
    repo = new UserRepository(db);
  });
  
  test('测试A', () => {
    // 直接开始测试
  });
  
  test('测试B', () => {
    // 另一个测试
  });
});

// 坏味道2:过于复杂的测试数据
// 重构前
test('复杂业务逻辑', () => {
  const order = {
    id: 1,
    customer: {
      id: 1001,
      name: '张三',
      email: 'zhangsan@example.com',
      address: {
        street: '人民路',
        city: '北京',
        zipCode: '100000'
      }
    },
    items: [
      {
        id: 101,
        name: '商品A',
        price: 100,
        quantity: 2,
        sku: 'SKU001',
        category: '电子产品',
        // ... 更多字段
      }
    ],
    // ... 更多嵌套
  };
  
  // 测试代码
});

// 重构后:使用构建器模式
test('复杂业务逻辑', () => {
  const order = new OrderBuilder()
    .withCustomer(
      new CustomerBuilder()
        .withName('张三')
        .withEmail('zhangsan@example.com')
        .withAddress('北京', '人民路', '100000')
        .build()
    )
    .withItem(
      new ItemBuilder()
        .withName('商品A')
        .withPrice(100)
        .withQuantity(2)
        .withCategory('电子产品')
        .build()
    )
    .build();
  
  // 测试代码
});

// 坏味道3:测试逻辑与断言混合
// 重构前
test('计算订单折扣', () => {
  const order = createTestOrder();
  const discount = calculateDiscount(order);
  
  // 复杂的断言逻辑
  if (order.customer.isVIP) {
    expect(discount).toBeGreaterThan(0.1);
    if (order.total > 1000) {
      expect(discount).toBe(0.2);
    } else {
      expect(discount).toBe(0.15);
    }
  } else {
    expect(discount).toBe(0);
  }
});

// 重构后:使用参数化测试
describe.each([
  { isVIP: true, total: 1200, expectedDiscount: 0.2 },
  { isVIP: true, total: 800, expectedDiscount: 0.15 },
  { isVIP: false, total: 1200, expectedDiscount: 0 },
  { isVIP: false, total: 800, expectedDiscount: 0 },
])('计算订单折扣 - VIP:$isVIP, 总价:$total', ({ isVIP, total, expectedDiscount }) => {
  test(`折扣应为${expectedDiscount}`, () => {
    const order = createTestOrder({ customer: { isVIP }, total });
    const discount = calculateDiscount(order);
    expect(discount).toBe(expectedDiscount);
  });
});

7. 测试驱动开发(TDD)与AI的结合

7.1 使用AI加速TDD流程

传统的TDD流程是:红(写失败测试)→ 绿(写最少代码使测试通过)→ 重构。AI可以在这个流程的每个阶段提供帮助:

// 阶段1:红 - AI帮助编写测试
// 提示词:为UserService的createUser方法编写测试,要求:
// 1. 测试正常创建用户
// 2. 测试邮箱重复的情况
// 3. 测试无效邮箱的情况
// 4. 测试密码强度不足的情况

// AI生成的测试骨架
describe('UserService.createUser', () => {
  let service: UserService;
  let mockRepo: jest.Mocked<UserRepository>;
  
  beforeEach(() => {
    mockRepo = {
      findByEmail: jest.fn(),
      save: jest.fn(),
    };
    service = new UserService(mockRepo);
  });
  
  test('创建新用户成功', async () => {
    // 待实现
  });
  
  test('邮箱已存在时抛出错误', async () => {
    // 待实现
  });
  
  test('无效邮箱格式时抛出错误', async () => {
    // 待实现
  });
  
  test('密码强度不足时抛出错误', async () => {
    // 待实现
  });
});

// 阶段2:绿 - AI帮助实现功能
// 提示词:实现UserService.createUser方法,满足以下测试要求:
// 1. 邮箱必须符合格式
// 2. 密码至少8位,包含大小写字母和数字
// 3. 邮箱不能重复
// 4. 成功创建后返回用户ID

// AI生成的实现
class UserService {
  constructor(private userRepo: UserRepository) {}
  
  async createUser(data: CreateUserDto): Promise<string> {
    // 验证邮箱格式
    if (!this.isValidEmail(data.email)) {
      throw new Error('无效的邮箱格式');
    }
    
    // 验证密码强度
    if (!this.isStrongPassword(data.password)) {
      throw new Error('密码强度不足');
    }
    
    // 检查邮箱是否已存在
    const existing = await this.userRepo.findByEmail(data.email);
    if (existing) {
      throw new Error('邮箱已注册');
    }
    
    // 创建用户
    const user = new User({
      email: data.email,
      passwordHash: await this.hashPassword(data.password),
      createdAt: new Date(),
    });
    
    const saved = await this.userRepo.save(user);
    return saved.id;
  }
  
  private isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
  
  private isStrongPassword(password: string): boolean {
    return password.length >= 8 &&
           /[a-z]/.test(password) &&
           /[A-Z]/.test(password) &&
           /\d/.test(password);
  }
  
  private async hashPassword(password: string): Promise<string> {
    // 实际项目中应该使用bcrypt等库
    return Buffer.from(password).toString('base64');
  }
}

// 阶段3:重构 - AI帮助改进代码
// 提示词:重构UserService,将邮箱验证和密码验证提取到独立的类中

// AI建议的重构
class EmailValidator {
  static isValid(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
  
  static normalize(email: string): string {
    return email.trim().toLowerCase();
  }
}

class PasswordValidator {
  static isStrong(password: string): boolean {
    return password.length >= 8 &&
           /[a-z]/.test(password) &&
           /[A-Z]/.test(password) &&
           /\d/.test(password);
  }
  
  static getStrengthScore(password: string): number {
    let score = 0;
    if (password.length >= 8) score++;
    if (/[a-z]/.test(password)) score++;
    if (/[A-Z]/.test(password)) score++;
    if (/\d/.test(password)) score++;
    if (/[^a-zA-Z0-9]/.test(password)) score++;
    return score;
  }
}

// 重构后的UserService
class UserService {
  constructor(private userRepo: UserRepository) {}
  
  async createUser(data: CreateUserDto): Promise<string> {
    if (!EmailValidator.isValid(data.email)) {
      throw new Error('无效的邮箱格式');
    }
    
    if (!PasswordValidator.isStrong(data.password)) {
      throw new Error('密码强度不足');
    }
    
    const normalizedEmail = EmailValidator.normalize(data.email);
    const existing = await this.userRepo.findByEmail(normalizedEmail);
    if (existing) {
      throw new Error('邮箱已注册');
    }
    
    const user = new User({
      email: normalizedEmail,
      passwordHash: await this.hashPassword(data.password),
      createdAt: new Date(),
    });
    
    const saved = await this.userRepo.save(user);
    return saved.id;
  }
  
  private async hashPassword(password: string): Promise<string> {
    return Buffer.from(password).toString('base64');
  }
}

7.2 测试作为文档的实践

好的测试本身就是最好的文档。AI可以帮助我们编写更具可读性的测试,这些测试可以作为代码的使用示例:

// 传统的测试(只关注正确性)
test('addUser adds user to repository', () => {
  const repo = new UserRepository();
  const service = new UserService(repo);
  service.addUser('Alice', 'alice@example.com');
  expect(repo.users).toHaveLength(1);
});

// 作为文档的测试(展示如何使用API)
describe('UserService API示例', () => {
  test('如何创建新用户', async () => {
    // 1. 创建服务实例
    const userService = new UserService(new InMemoryUserRepository());
    
    // 2. 调用createUser方法
    const userId = await userService.createUser({
      name: 'Alice',
      email: 'alice@example.com',
      password: 'SecurePass123'
    });
    
    // 3. 验证用户已创建
    expect(userId).toBeDefined();
    
    // 4. 可以获取用户信息
    const user = await userService.getUser(userId);
    expect(user.name).toBe('Alice');
    expect(user.email).toBe('alice@example.com');
  });
  
  test('如何处理重复邮箱', async () => {
    const userService = new UserService(new InMemoryUserRepository());
    
    // 第一次创建成功
    await userService.createUser({
      name: 'Alice',
      email: 'alice@example.com',
      password: 'SecurePass123'
    });
    
    // 第二次使用相同邮箱会失败
    await expect(
      userService.createUser({
        name: 'Alice2',
        email: 'alice@example.com',  // 重复邮箱
        password: 'AnotherPass456'
      })
    ).rejects.toThrow('邮箱已注册');
  });
  
  test('如何验证用户密码', async () => {
    const userService = new UserService(new InMemoryUserRepository());
    
    // 创建用户
    const userId = await userService.createUser({
      name: 'Alice',
      email: 'alice@example.com',
      password: 'MySecretPassword'
    });
    
    // 使用正确密码验证
    const isValid = await userService.verifyPassword(userId, 'MySecretPassword');
    expect(isValid).toBe(true);
    
    // 使用错误密码验证
    const isInvalid = await userService.verifyPassword(userId, 'WrongPassword');
    expect(isInvalid).toBe(false);
  });
  
  test('如何更新用户信息', async () => {
    const userService = new UserService(new InMemoryUserRepository());
    const userId = await userService.createUser({
      name: 'Alice',
      email: 'alice@example.com',
      password: 'password123'
    });
    
    // 更新用户姓名
    await userService.updateUser(userId, { name: 'Alice Smith' });
    
    const updatedUser = await userService.getUser(userId);
    expect(updatedUser.name).toBe('Alice Smith');
    expect(updatedUser.email).toBe('alice@example.com'); // 邮箱不变
  });
});

7.3 测试生成与维护的自动化

随着项目规模增长,手动维护测试变得越来越困难。可以建立一些自动化流程:

// 自动化测试生成脚本
// scripts/generate-tests.js
const fs = require('fs');
const path = require('path');
const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');

function generateTestsForFile(filePath) {
  const code = fs.readFileSync(filePath, 'utf-8');
  const ast = parse(code, {
    sourceType: 'module',
    plugins: ['typescript', 'jsx']
  });
  
  const tests = [];
  
  traverse(ast, {
    FunctionDeclaration(path) {
      const funcName = path.node.id.name;
      const params = path.node.params.map(p => p.name);
      
      // 跳过私有函数(以下划线开头)
      if (funcName.startsWith('_')) return;
      
      // 生成测试模板
      const testCode = `
describe('${funcName}', () => {
  test('应该处理正常情况', () => {
    // TODO: 添加测试逻辑
  });
  
  test('应该处理边界情况', () => {
    // TODO: 添加边界测试
  });
  
  test('应该处理错误情况', () => {
    // TODO: 添加错误处理测试
  });
});
`;
      
      tests.push({
        function: funcName,
        testCode: testCode.trim()
      });
    },
    
    ClassDeclaration(path) {
      const className = path.node.id.name;
      
      // 收集公有方法
      const publicMethods = [];
      path.traverse({
        ClassMethod(classMethodPath) {
          const methodName = classMethodPath.node.key.name;
          // 跳过私有方法和构造函数
          if (methodName.startsWith('_') || methodName === 'constructor') return;
          
          publicMethods.push(methodName);
        }
      });
      
      if (publicMethods.length > 0) {
        const testCode = `
describe('${className}', () => {
${publicMethods.map(method => `
  describe('${method}', () => {
    test('应该正确执行', () => {
      // TODO: 测试${method}方法
    });
  });
`).join('\n')}
});
`;
        
        tests.push({
          class: className,
          testCode: testCode.trim()
        });
      }
    }
  });
  
  return tests;
}

// 使用示例
const tests = generateTestsForFile('./src/utils/math.ts');
console.log('生成的测试模板:');
tests.forEach((test, index) => {
  console.log(`\n// 测试 ${index + 1}`);
  console.log(test.testCode);
});

// 测试覆盖率监控脚本
// scripts/monitor-coverage.js
const { execSync } = require('child_process');
const fs = require('fs');

function getCoverageReport() {
  try {
    // 运行测试并生成覆盖率报告
    execSync('npm test -- --coverage', { stdio: 'inherit' });
    
    // 读取覆盖率报告
    const coverageSummary = JSON.parse(
      fs.readFileSync('./coverage/coverage-summary.json', 'utf-8')
    );
    
    const totals = coverageSummary.total;
    
    console.log('\n=== 测试覆盖率报告 ===');
    console.log(`行覆盖率: ${totals.lines.pct}%`);
    console.log(`语句覆盖率: ${totals.statements.pct}%`);
    console.log(`分支覆盖率: ${totals.branches.pct}%`);
    console.log(`函数覆盖率: ${totals.functions.pct}%`);
    
    // 检查是否达标
    const thresholds = {
      lines: 80,
      statements: 80,
      branches: 70,
      functions: 90
    };
    
    let passed = true;
    for (const [metric, threshold] of Object.entries(thresholds)) {
      if (totals[metric].pct < threshold) {
        console.log(`❌ ${metric}覆盖率 ${totals[metric].pct}% < ${threshold}%`);
        passed = false;
      } else {
        console.log(`✅ ${metric}覆盖率 ${totals[metric].pct}% >= ${threshold}%`);
      }
    }
    
    // 找出覆盖率低的文件
    console.log('\n=== 覆盖率低的文件 ===');
    Object.entries(coverageSummary).forEach(([filePath, coverage]) => {
      if (filePath !== 'total' && coverage.lines.pct < 80) {
        console.log(`${filePath}: ${coverage.lines.pct}% 行覆盖率`);
      }
    });
    
    return passed;
  } catch (error) {
    console.error('生成覆盖率报告失败:', error.message);
    return false;
  }
}

// 集成到package.json
// package.json
{
  "scripts": {
    "test": "jest",
    "test:coverage": "node scripts/monitor-coverage.js",
    "test:watch": "jest --watch",
    "test:generate": "node scripts/generate-tests.js",
    "test:quality": "bash ~/.claude/skills/anti-test-theater/scripts/check-test-quality.sh src/"
  }
}

8. 测试文化的建设与推广

8.1 建立团队的测试思维

工具和技术只是基础,更重要的是建立团队的测试文化。以下是一些实践建议:

定期举办测试工作坊

  • 每月一次,分享测试最佳实践
  • 演示如何使用 anti-test-theater 改进测试
  • 代码审查中重点关注测试质量
  • 分享测试失败的教训和解决方案

建立测试知识库

# 测试知识库

## 常见问题与解决方案

### 问题:测试运行太慢
**解决方案**:
1. 使用jest的`--maxWorkers`选项并行运行测试
2. 将慢测试标记为`@slow`,在CI中单独运行
3. 使用内存数据库代替真实数据库
4. Mock外部API调用

### 问题:测试不稳定(Flaky Tests)
**解决方案**:
1. 避免固定时间的等待,使用`waitFor`或`findBy*`
2. 使用可重试的测试工具
3. 隔离有副作用的测试
4. 使用测试容器确保环境一致性

### 问题:测试维护成本高
**解决方案**:
1. 使用工厂函数和构建器模式创建测试数据
2. 将通用测试逻辑提取到工具函数中
3. 使用参数化测试减少重复
4. 定期重构测试代码

## 测试模式库

### 数据库测试模式
```typescript
// 使用事务回滚
test('数据库操作', async () => {
  await db.transaction(async (trx) => {
    // 在事务中执行操作
    await trx.insert(users).values({ name: 'Test' });
    
    // 测试断言
    const result = await trx.select().from(users);
    expect(result).toHaveLength(1);
    
    // 事务会自动回滚,不污染数据库
  });
});

// 使用测试夹具
beforeAll(async () => {
  // 设置测试数据
  await db.insert(users).values([
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ]);
});

afterAll(async () => {
  // 清理测试数据
  await db.delete(users);
});

API测试模式

// 测试请求验证
test('验证请求参数', async () => {
  const response = await request(app)
    .post('/api/users')
    .send({ email: 'invalid' }); // 缺少必要字段
  
  expect(response.status).toBe(400);
  expect(response.body.errors).toContain('email');
});

// 测试认证和授权
test('需要认证的端点', async () => {
  // 未认证的请求
  const unauthResponse = await request(app).get('/api/profile');
  expect(unauthResponse.status).toBe(401);
  
  // 已认证的请求
  const authResponse = await request(app)
    .get('/api/profile')
    .set('Authorization', 'Bearer valid-token');
  expect(authResponse.status).toBe(200);
});

测试工具推荐

单元测试

  • Jest : JavaScript/TypeScript测试框架
  • Vitest : 更快的Vite原生测试框架
  • Mocha + Chai : 灵活的测试组合

集成测试

  • Supertest : API端点测试
  • MSW :

更多推荐