行为测试技能Skill behavior-testing

行为测试技能是一种专注于验证软件应用功能行为的测试方法,确保API调用、状态管理、错误处理和安全性的正确性。它弥补了传统渲染测试的不足,提升测试覆盖率和应用质量。关键词:行为测试、API测试、状态持久性、错误边界、安全测试、软件测试、功能验证。

测试 0 次安装 0 次浏览 更新于 3/18/2026

行为测试技能

触发条件

  • 功能实现后
  • 当测试只检查DOM渲染(“冒烟测试”)
  • 当代码审查发现测试覆盖率不足
  • /behavior-tests 斜杠命令

问题

标准测试(通过AI生成或Copilot)通常只检查"是否渲染?“,而不是"是否工作?”。这导致:

  • 没有功能的按钮通过所有测试
  • API调用使用错误参数从未被发现
  • 导航/重新挂载时的状态丢失不可见
  • 缺少错误UI从未被测试

4个测试类别(强制)

1. API合同测试

是什么: 验证API函数是否以正确的参数被调用。 为什么: 错误参数(缺少语言参数、错误端点)在渲染测试中不可见。

// 差:只检查DOM
it('发送消息', async () => {
  await sendMessage('hello');
  expect(screen.getByText('hello')).toBeInTheDocument(); // 仅DOM!
});

// 好:检查API合同
it('以正确语言发送消息', async () => {
  await sendMessage('hello');
  expect(mockApi).toHaveBeenCalledWith(
    'hello',
    expect.any(Array),   // 历史
    'en',                 // 语言代码
  );
});

检查清单:

  • [ ] 使用vi.fn()模拟API函数
  • [ ] 使用toHaveBeenCalledWith检查调用参数
  • [ ] 使用toHaveBeenCalledTimes检查调用次数
  • [ ] 检查API错误时发生什么(错误UI可见?)

2. 状态持久性测试

是什么: 验证在重新挂载、页面刷新或认证切换后,状态是否正确保持。 为什么: sessionStorage/localStorage错误,登录/注销时的状态重置。

// 检查sessionStorage持久性
it('将计数器持久化到sessionStorage', async () => {
  render(<Chat />);
  await sendMessage('test');
  expect(sessionStorage.getItem('key')).toBe('1');
});

// 检查认证转换
it('登录时重置计数器', () => {
  sessionStorage.setItem('key', '5');
  useAuthStore.setState({ isAuthenticated: true });
  // 效果应该重置
  expect(sessionStorage.getItem('key')).toBe('0');
});

检查清单:

  • [ ] 在操作前后检查sessionStorage/localStorage
  • [ ] 测试认证状态切换(登录 -> 登出及反之)
  • [ ] 直接使用setState操作状态存储以处理边缘情况
  • [ ] 检查存储重置后的默认状态

3. 错误边界测试

是什么: 验证错误是否向用户显示,并且应用不会崩溃。 为什么: 缺少错误UI是最常见的用户体验缺陷。

// API错误 -> 错误UI
it('结账失败时显示错误', async () => {
  mockPost.mockRejectedValueOnce(new Error('服务器错误'));
  fireEvent.click(checkoutButton);
  await waitFor(() => {
    expect(screen.getByText(/error|fehler/i)).toBeInTheDocument();
  });
});

// 204无内容 -> 不崩溃
it('处理204无内容', async () => {
  mockFetch({ ok: true, status: 204, text: () => Promise.resolve('') });
  const result = await apiClient.get('/api/logout');
  expect(result).toEqual({});
});

检查清单:

  • [ ] API拒绝 -> 错误消息可见
  • [ ] 空响应(204,空体) -> 不崩溃
  • [ ] 速率限制(429) -> 显示重试信息
  • [ ] 非JSON错误体 -> 优雅降级

4. 安全边界测试

是什么: 验证输入验证、认证门和重定向安全性。 为什么: XSS预防、开放重定向防止、速率限制执行。

// 输入长度
it('拒绝超过2000字符的输入', async () => {
  await sendMessage('a'.repeat(2001));
  expect(mockApi).not.toHaveBeenCalled();
  expect(screen.getByText(/maximum/i)).toBeInTheDocument();
});

// URL验证
it('阻止非stripe.com重定向URL', async () => {
  mockPost.mockResolvedValueOnce({ url: 'https://evil.com/steal' });
  fireEvent.click(checkoutButton);
  await waitFor(() => {
    expect(screen.getByText(/error/i)).toBeInTheDocument();
  });
});

// CSRF令牌
it('将CSRF令牌附加到POST但不附加到GET', async () => {
  document.cookie = 'csrf_token=abc123';
  await apiClient.post('/api/data', {});
  expect(fetchMock).toHaveBeenCalledWith(
    expect.any(String),
    expect.objectContaining({
      headers: expect.objectContaining({ 'X-CSRF-Token': 'abc123' }),
    }),
  );
});

检查清单:

  • [ ] 输入长度限制在客户端强制执行
  • [ ] 认证门阻止未经认证的用户
  • [ ] 计划门在高级功能中阻止免费计划
  • [ ] 重定向URL检查允许的域名
  • [ ] CSRF令牌在POST中附加,不在GET中
  • [ ] 在所有请求中设置Credentials: ‘include’

实施工作流程

  1. 识别缺陷:阅读现有测试。检查它们是否只使用toBeInTheDocument()
  2. 模拟APIs:使用vi.mock()用于服务模块,vi.fn()用于单个函数。
  3. 直接设置状态:使用useAuthStore.setState({...})代替通过UI导航。
  4. 检查调用toHaveBeenCalledWith()toBeInTheDocument()更重要。
  5. 测试错误路径:生产代码中的每个try/catch都需要一个测试。

文件命名

# 保留现有渲染测试:
ComponentName.test.tsx

# 新的行为测试放在旁边:
ComponentName.behavior.test.tsx

# 或在describe块中分离:
describe('rendering', () => { ... });   // 现有
describe('behavior', () => { ... });    // 新
describe('error handling', () => { ... }); // 新

反模式

反模式 问题 解决方案
仅使用toBeInTheDocument() 不检查按钮是否工作 toHaveBeenCalledWith()
没有API模拟 测试调用真实API vi.mock() + vi.fn()
只测试快乐路径 错误UI从未被测试 mockRejectedValueOnce()
状态假设 存储未重置 beforeEach + setState
没有认证上下文 测试只作为登录用户运行 测试两种状态

参考实现

参见:Website/Relaunch 2026 01/services/__tests__/apiClient.test.ts(错误边界 + 安全边界模式) 参见:Website/Relaunch 2026 01/components/__tests__/MultilingualChat.behavior.test.tsx(API合同 + 状态持久性模式)