测试分层方法论

做了一段时间无障碍测试后,我发现很多人都有一个误解——以为写几个端到端用例就万事大吉了。其实不然。测试也需要分层,就像盖楼一样,每层承担不同的责任。这一章我来聊聊分层测试策略,以及单元、集成、E2E这三层的职责该怎么划分。

为什么需要分层测试?

你想想看,如果只测最上层,底下的代码出了问题,定位起来就像大海捞针。我自己经历过一个项目,团队只写E2E测试,跑一轮要40分钟。结果一个低级错误——某个函数返回了undefined,E2E用例在100步之后才报错,定位花费了两天。从那以后,我坚决推行分层测试。

分层的好处很明显:

  • 反馈速度:单元测试毫秒级,集成测试秒级,E2E分钟级。你不需要每次都跑全量。
  • 定位精准:哪一层失败,问题大概率就在哪一层。
  • 维护成本:底层测试更稳定,不会因为UI变动频繁失效。
我的建议:用「测试金字塔」来指导分层比例。底层单元测试要最多,中间集成测试适中,顶层E2E最少。

分层测试策略总览

我把这三层画成了一张图,你看一下就能明白它们之间的关系。

测试分层策略——金字塔与职责 单元测试层 函数 / 方法 / 组件 数量最多 · 速度最快 集成测试层 接口 · 服务 · 模块间交互 数量适中 · 速度中等 E2E测试层 用户场景 · 关键路径 数量最少 · 速度最慢 💡 关键原则: 最底层最多用例 越往上越少 覆盖面 慢 快 执行速度 慢 快

单元层:从最小粒度开始

单元测试,说白了就是测一个函数、一个方法、一个独立的组件。它的核心目标是验证逻辑正确性。无障碍测试里,单元层通常测什么?我举几个例子:

  • ARIA属性计算:比如某个函数根据角色和状态返回正确的aria-label
  • 焦点管理:键盘事件处理函数是否正确地移动了焦点
  • 颜色对比度工具函数:传入两个颜色值,返回是否符合WCAG标准
  • 屏幕阅读器文本生成:拼接逻辑是否包含了必要的替代文本

我在项目中曾经遇到过一个问题:一个按钮组件,当disabled属性为true时,应该自动添加aria-disabled="true"。但单元测试只有两个用例——属实和false。后来我补了一个边界用例:传入undefined,结果发现代码直接抛错了。这种问题在集成层和E2E层很难第一时间发现,但单元测试一下就抓住了。

注意陷阱:单元测试不要过度mock。我曾经见过一个团队把整个DOM都mock掉了,结果单元测试全绿,但实际页面根本就没有那个元素。好的单元测试应该mock最外层依赖,但内部逻辑一定要真实跑通。

集成层:把碎片拼起来

单元测完,接下来就是集成层。这一层关注的是多个单元之间的协作。无障碍测试里,最常见的是组件之间的交互,比如:

  • 弹窗打开时,焦点是否自动移动到弹窗内
  • 关闭弹窗后,焦点是否回到触发按钮
  • 表单验证失败时,错误信息是否与输入框正确关联
  • 导航菜单展开/折叠时,aria-expanded状态是否同步

我个人的习惯是:集成测试用真实DOM渲染(比如JSDOM或HappyDOM),但不要启动真实浏览器。这样可以跑得很快。举个例子,一个下拉菜单组件,你需要测试:点击按钮→菜单出现→按Escape→菜单关闭,并且焦点回到按钮。集成测试里,你直接操作DOM事件,断言DOM变化和焦点状态。

// 集成测试示例(使用Jest + Testing Library)
test('下拉菜单按Escape关闭后焦点回退到触发器', () => {
    render(<Dropdown />);
    const trigger = screen.getByRole('button', { name: /菜单/ });
    trigger.focus();
    fireEvent.click(trigger);
    expect(screen.getByRole('menu')).toBeVisible();
    
    fireEvent.keyDown(screen.getByRole('menu'), { key: 'Escape' });
    expect(document.activeElement).toBe(trigger);
});

你想想看,这种测试在单元层写不了——因为它涉及多个组件;在E2E层写又太慢——还要开浏览器。放在集成层刚刚好。

E2E层:用户视角的最终验证

E2E测试模拟真实用户的操作,用真正的浏览器、屏幕阅读器(或者模拟辅助技术)来验证整个流程。这一层用例最少,但最关键。我的原则是:每条E2E用例必须覆盖一条用户核心路径。

无障碍方面的E2E示例:

  • 用户使用Tab键遍历整个表单并成功提交
  • 屏幕阅读器朗读商品详情页时,所有关键信息都能被读到
  • 使用WCAG的对比度检查插件扫描全页,确保都通过
  • 改变窗口大小到320px,查看内容是否仍然可操作
关键指标:E2E用例数量通常占整个测试套件的5%~10%。如果超过20%,你就要反思是不是底层测试不够了。

我曾经犯过一个错误:把一个非常复杂的多步表单流程写成了E2E用例,结果每次CI都要花5分钟跑这个用例。后来我把它拆分成:单元层测试每个字段的验证逻辑,集成层测试表单提交的数据组装,E2E只验证完整流程能走通。时间降到了30秒,而且定位问题更快。

各层职责对比

维度 单元层 集成层 E2E层
测试对象 函数 / 方法 / 独立组件 组件组合 / 服务 / 接口 完整用户场景
执行环境 Node / 纯JS JSDOM / HappyDOM / 轻量浏览器 真实浏览器(Chrome/Firefox等)
无障碍关注点 ARIA属性、焦点函数、颜色计算 焦点流转、角色状态同步、关联关系 键盘导航、屏幕阅读器朗读、对比度验证
执行速度 毫秒级 秒级 分钟级
数量占比 60%~70% 20%~30% 5%~10%

避坑指南:我踩过的三个坑

  1. 单元测试mock过度:我曾经把整个DOM API全部mock了,结果测试通过了但页面根本不渲染。现在我会用渲染库(如@testing-library/react)来保持真实DOM行为。
  2. 集成测试和E2E测试混淆:有些团队把所有涉及UI的都扔到E2E层,结果CI非常慢。后来我明确了一条规则——只要不需要真浏览器键盘事件与屏幕阅读器交互,就放在集成层。
  3. E2E用例没有优先级:一开始我们把所有功能都用E2E覆盖,跑一次4小时。后来我们只保留最核心的5条无障碍路径,其他的用集成测。效率大幅提升。
小窍门:每次提交代码前,先跑单元测试(几分钟);合入PR时跑集成测试(十几分钟);发布前跑E2E(半小时)。通过率分别达到100%、95%、90%以上才能放心上线。