人人都会AI编程

19.3 组件命名、文件命名、目录结构规范

更新时间:2026-07-10

测试覆盖率是衡量测试完整性的重要手段,但它不是最终目标。合理使用覆盖率数据,结合有效的测试用例设计原则,才能真正提升项目的质量和维护信心。

19.3.1 理解测试覆盖率

覆盖率度量的是代码在测试执行过程中被触发的比例。常见指标包括:

  • 语句覆盖率(Statement):每个可执行语句是否都被执行过。
  • 分支覆盖率(Branch):每个条件语句(如 if-elseswitch、三元运算符)的各个分支是否都被覆盖。
  • 函数覆盖率(Function):每个函数是否至少被调用一次。
  • 行覆盖率(Line):每一行可执行代码是否都被执行过。

在 React 项目中,可通过 Jest 的内置覆盖率工具或 Istanbul(nyc)生成报告。

# Jest 直接输出覆盖率
npx jest --coverage

package.json 中配置覆盖率阈值,确保关键指标不下降:

{
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts",
      "!src/index.tsx",
      "!src/reportWebVitals.ts"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

collectCoverageFrom 明确统计范围,排除类型声明、入口文件等无需测试的部分。阈值不追求 100%,而是根据业务关键度设置合理的底线。

19.3.2 覆盖率数据的正确使用

高覆盖率不等于高质量测试。下面几种情况仍可能存在风险:

  • 只测了代码路径,未断言行为:测试调用了函数但没有检查返回值或副作用是否正确。
  • 缺少边界和异常场景:仅覆盖常规流程,未测试空数据、错误响应、极端输入等。
  • 实现细节耦合:测试验证组件内部状态变量而非用户可见的界面,重构时测试会大量失效。

覆盖率应被视为 “未覆盖区域的发现工具”,而非质量证明。重点审查覆盖率报告中未覆盖的逻辑分支,评估是否存在业务风险。

19.3.3 测试用例设计核心原则

原则一:从用户行为出发,而非实现细节

组件测试应模拟用户操作和观察渲染结果,避免直接测试内部 state、方法名或 Hooks 调用顺序。

// ❌ 绑定实现细节:直接检查 state
test('计数器组件 state 正确', () => {
  const { result } = renderHook(() => useCounter());
  expect(result.current.count).toBe(0);
});

// ✅ 从用户视角:验证 UI 展示与交互效果
test('点击按钮后计数显示增加', () => {
  render(<Counter />);
  const button = screen.getByRole('button', { name: /增加/ });
  fireEvent.click(button);
  expect(screen.getByText(/计数:1/)).toBeInTheDocument();
});

这样的测试在重构组件内部实现(如 state 变量重命名)时依然有效。

原则二:AAA 模式组织测试

每个测试用例按照 Arrange(准备)→ Act(行为)→ Assert(断言) 的结构清晰编写。

test('删除待办事项后列表更新', async () => {
  // Arrange:渲染组件并填充初始数据
  render(<TodoList />);
  await screen.findByText('学习测试');

  // Act:点击删除按钮
  fireEvent.click(screen.getByRole('button', { name: /删除 学习测试/ }));

  // Assert:待办事项不再存在
  expect(screen.queryByText('学习测试')).toBeNull();
});

这种模式让测试意图一目了然,降低维护成本。

原则三:覆盖边界与异常情况

正常路径的测试仅能满足基本验证,健壮的应用必须覆盖:

  • 空数据或默认状态:列表为空时是否显示占位提示。
  • 错误状态:API 请求失败时是否显示错误信息并重试。
  • 极限值:输入超长字符串、数量为 0 或负值等。
  • 并发操作:快速双击按钮是否导致重复请求。
  • 权限缺失:无权限用户能否看到或操作受限内容。

示例:测试空列表状态

test('任务列表为空时显示空状态提示', () => {
  // 模拟接口返回空数组
  jest.spyOn(api, 'fetchTasks').mockResolvedValue([]);
  render(<TaskBoard />);
  expect(screen.getByText('暂无任务,创建第一个吧')).toBeInTheDocument();
});

原则四:保持测试独立性

每个测试用例不应依赖其他测试的执行顺序或共享的可变状态。Jest 默认并行运行测试,共享状态可能导致不可预测的失败。每个测试应自行准备所需数据(通过渲染或模拟),并在清理阶段(afterEach)重置模拟。

afterEach(() => {
  jest.clearAllMocks();
  cleanup(); // React Testing Library 已自动处理
});

原则五:测试可读性优先

测试代码是文档的一部分。用例名称应明确描述行为和预期结果,使用动词和业务语义。

// ❌ 模糊命名
test('按钮功能正常', () => { ... });

// ✅ 明确命名
test('点击"提交"后,按钮禁用并显示加载状态', () => { ... });

当测试失败时,从名称就能快速理解哪个业务场景被破坏。

19.3.4 平衡覆盖率与维护成本

在实际项目中,100% 覆盖率往往带来边际效益递减。建议策略:

  • 核心业务逻辑:工具函数、复杂状态机、权限控制等,追求高分支覆盖率(≥90%)。
  • UI 组件:重点覆盖交互路径和异常状态,不强迫覆盖所有视觉变体。
  • 第三方库封装:简单封装(如一行 axios.get)可忽略单测,依赖 E2E 覆盖。
  • 常量或类型定义:无需测试。

记住:测试的信心价值远高于数字指标。一个覆盖关键流程、边界和错误路径的测试集,比一个刷满覆盖率但断言薄弱的测试集有用得多。

19.3.5 实战:为一组件设计完整测试套件

以一个 SearchInput 组件为例,展示完整的测试用例设计:

function SearchInput({ onSearch }) {
  const [value, setValue] = useState('');
  const handleSubmit = (e) => {
    e.preventDefault();
    if (value.trim()) onSearch(value.trim());
  };

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

测试用例列表:

  1. 正常搜索:输入文本,点击搜索,回调被调用且参数正确。
  2. 空白输入拦截:输入空格或留空,提交时不触发回调。
  3. 去除首尾空格:输入“ hello ”,回调参数应为“hello”。
  4. 表单提交事件:通过键盘回车键也能触发搜索。
  5. 清空后再次搜索:输入、清除、再输入,验证每次搜索结果正确。

通过遵循设计原则,该测试套件稳固且对重构友好。


测试覆盖率是工具,测试用例设计是艺术。两者结合才能构建出真正守护代码质量的测试体系。