大多数端到端测试套件都会使用真实的浏览器,模拟用户操作来测试应用程序。它们会检查页面是否能够正确显示,以及各种元素是否能够正常呈现。

但这些测试并不会验证这些元素上显示的数值是否准确。例如,如果数据传输过程中出现错误,导致马来西亚的人口数量被显示为340万而不是实际的3400万,这样的错误也会在所有的测试中被忽略。

那些元素依然存在,数值也依然会被正确显示,页面看起来也没有问题。但这个错误就会就这样被发布出去,直到有人注意到它为止。

我是一名全栈工程师。使用Playwright编写端到端测试,使用Jest编写单元测试,这些都是我的日常工作内容。当需要快速生成测试代码或调试程序流程时,我也会使用Playwright MCP——这个工具能够将像Claude这样的AI助手与正在运行的浏览器连接起来。

然而,没有任何这些工具能够完全解决基于选择器的测试套件所存在的问题。我在工作中维护的每一个端到端测试套件,都会逐渐积累大量的data-testid选择器、waitForSelector调用语句,以及那些因为某个按钮的名称被修改而失效的测试用例。

在Bug0举办的应用程序故障调试竞赛中,我得到了尝试某种新方法的机会。在一个周末的时间里,我使用Bug0开发的开源AI驱动的Playwright库Passmark,为马来西亚的三个公共开放数据门户网站——data.gov.myOpenDOSMKKMNow——构建了一个自动化的回归测试套件。

这些测试用例是用简单的英语编写的。有两个AI模型会用来验证每一个测试断言,而第三个模型则会在出现分歧时进行裁决。

以下内容您将看到:

  • 如何编写端到端测试,以验证仪表板上的数值是否准确,而不仅仅是页面是否能够正确显示

  • 一种特定的断言模式(用于处理范围受限的KPI指标),这种模式能够捕捉到那些选择器测试无法发现的数据传输错误,同时还会提供可供直接复用的示例代码

  • 一种跨领域的数学验证方法,使用Passmark提供的描述语句,有时甚至只需要几十行代码即可完成测试

  • Passmark自带的故障解释功能是如何成为我调试工具的;这一改变对我今后编写端到端测试的方式产生了深远的影响

  • 这些方法也存在一些实际的限制:例如缓存命中率仅为14%,还需要依赖OpenRouter,而且某些问题也无法通过双模型投票机制来检测出来

目录

为什么选择马来西亚的开放数据门户?

在那次黑客马拉松活动中,人们提出了诸如Vercel Commerce、Cal.com和Hashnode这样的目标。这些确实都是不错的选择。

但我更想测试一些与我的日常工作密切相关的内容。同时,我也需要选择一个数据量较大的网站来进行测试——因为我在日常生活中也会经常处理各种数字信息,所以确保屏幕上显示的数值准确是非常重要的。

马来西亚共有三个公开的开放数据门户:

  • data.gov.my,由政府的数字化转型机构MAMPU负责运营

  • OpenDOSM,由马来西亚统计局负责运营

  • KKMNow,由马来西亚卫生部负责运营

这些数据门户都是公开的,使用它们不需要进行任何身份验证,并且提供了文档化的API接口。因此,它们非常适合用来构建自动化测试套件。而这些数据正是马来西亚民众每天都在使用的信息,所以数据的准确性绝对不能有丝毫差错。

什么是Passmark?

Passmark是一个基于Playwright开发的测试工具库,其中的测试用例编写方式类似于技术规范书。下面来看一个例子:

await runSteps({
  page,
  userFlow: "population dashboard smoke",
  steps: [
    { description: "导航到https://data.gov.my/dashboard/kawasanku" },
    {
      description: "等待马来西亚国家层面的数据页面加载完成",
      waitUntil: "能够看到关于人口数量的标题信息",
    },
  ],
  assertions: [
    {
      assertion:
        "页面上显示的马来西亚总人口数应该大于2000万且小于4000万",
    },
  ],
  test,
  expect,
});

在Passmark中,不需要使用任何选择器、dataTestIdpage.locator()这类元素。测试用例的表述方式直接反映了我的需求,也是我会用来说服同事的语言。

在第一次运行测试时,人工智能会自动控制页面的交互流程,并将执行结果缓存到Redis中。之后的每次测试都会以Playwright本身的运行速度进行,而不会再次调用任何模型。

当用户界面发生变化导致之前缓存的测试用例无法正常执行时,人工智能只会针对那个具体的步骤重新进行判断。此时会有两个不同的推理模型(Claude和Gemini)参与评估,第三个模型则会负责协调两者之间的分歧。

“英雄规范”:范围限定型断言

范围限定型断言是我最初编写测试用例时使用的方法,也是在整个测试套件中我最常使用的类型。

这种测试方法的思路非常简单:只需要检查页面上显示的数值是否落在某个合理的范围内,而不需要确认某个特定元素是否存在。

下图是针对人口数据编写的Playwright测试报告,其中所有的四个范围限定型断言都通过了测试。

Playwright生成的HTML测试报告,用于验证人口数据。Passmark的标注显示:‘2025年马来西亚的总人口数为3420万,这个数值确实位于2000万到4000万的范围内。’所有四个范围限定型断言都通过。” height=

范围限定型检测方法恰恰能够体现Passmark工具的真正价值。

传统的检测方法主要依赖于DOM结构来进行判断。例如,它会确认带有kpi-total类的元素是否确实显示了“3420万”这个数值。但这种方法只能说明页面内容是如何呈现的,并不能判断这个数值本身是否合理。

有一种缺陷会导致马来西亚的人口数量被错误地显示为“342万”,然而这种缺陷并不会被任何常规的检测方法发现。因为DOM结构是正确的,数值也确实被渲染出来了,从表面上看没有任何问题。

Passmark工具会读取页面内容并对其进行验证,但由于“342万”这个数值超出了合理的范围,因此它会判定检测失败。虽然有两组模型参与了验证过程,但仅有一个模型出现错误判断,也是无法让系统误判为合格的。

两种模型联合验证也难以发现的问题

这种联合验证机制能够有效防止某个模型误解页面内容,但它无法阻止两个模型以完全相同的方式误解页面信息。例如,如果Claude和Gemini两个模型都因为DOM中某些不常见的格式问题,将“3240万”误解读为“324万”,那么它们都会给出“通过”的判断结果,从而导致缺陷被忽略。

为了解决这个问题,可以采用断言设计的方法。编写那些不容易被误解的断言语句。例如,使用范围限定来进行验证(比如“数值应在2000万到4000万之间”),这样的断言比描述性的语句更难被错误理解。数字范围的限定能减少解释的空间,而形容词则更容易引起歧义。因此,你的断言语句越像是用英语编写的单元测试代码,模型们就越不容易产生不同的解读结果。

更进一步:跨领域数学分析

范围限定型的断言确实是一个很好的起点,它们能够帮助我们判断“这个数值是否在合理的范围内”,但它们无法验证“这些数值之间是否相互吻合”。

为此,我们需要运用跨领域的数学分析方法。例如,如果一个数据面板同时展示了总人口数和按性别划分的人口数据,那么这两组数据应该是相符的:男性人口加上女性人口应该等于总人口数,各民族的人口占比加起来也应该等于100%。

test("跨领域数学验证:性别分类数据与总人口数是否一致", async ({ page }) => {
  test.setTimeout(180_000);
  await runSteps({
    page,
    userFlow: "population sex breakdown consistency",
    steps: [
      { description: "导航到https://data.gov.my/dashboard/kawasanku" },
      {
        description: "等待页面显示马来西亚的国家级数据视图及分类数据",
        waitUntil:
          "页面上能看到总人口数的标题数字,并且能看到按性别划分的数据",
      },
    ],
    assertions: [
      {
        assertion:
          "页面上显示的男性人口数和女性人口数之和应接近总人口数,误差范围应在5%以内",
      },
      {
        assertion:
          "页面上显示的各类人口占比之和应接近100%,误差范围应在2个百分点以内",
      },
      {
        assertion: "没有任何一项分类数据的数值是负数,也不应该超过总人口数",
      },
    ],
    test,
    expect,
  });
});

试着用普通的Playwright来编写这样的代码吧。你需要为标题中的数字设置选择器,为各个细分数据设置选择器,使用能够识别逗号的正则表达式来进行数值解析,同时还需要计算相应的间距。总共需要七八十到一百行代码才能实现这些功能,而一个小学生也能轻易看出其中三个逻辑是显而易见的。

Passmark版本的测试规范只包含一项要求。我用它来检测Kawasanku网站上的实时国家数据展示页面,结果在1.4分钟内就通过了所有三项验证。Passmark的评论原文如下:

“标题中显示了‘马来西亚的人口为32,447,385人’这一总数,同时也展示了‘性别与年龄分布’信息,这意味着可以进一步获取按性别划分的数据。”

有两个测试模型会读取页面内容,提取出相关数字并进行计算,如果结果一致,则认为测试通过。即使三个月后该数据展示页面的布局发生了变化,这些测试依然有效,因为当初编写代码时并没有指定具体的选择器。

对于我接触到的每一个数据展示产品,我都希望进行这类测试:确保财务汇总数据与其明细项目相符,百分比之和为100%,库存数量与各个仓库中的实际数量一致。不过如今很少有人会去做这样的检查,因为手动验证这些内容所花费的时间,远远超过了进行这种测试所带来的价值。

三次测试中我发现的问题

测试次数 通过项数 主要修改内容
1 13项中的4项(31%) 基于初始版本编写测试规范,没有查看目标页面的实际内容。
2 13项中的8项(62%) 根据Passmark提供的反馈,修改了5项描述过于详细的测试要求。
3 13项中的12项(92%) 去掉了另外1项错误的测试要求,调整了超时设置,增加了重试机制,并安装了WebKit插件。

Playwright的HTML报告页面,显示了第三次测试的结果:11项测试通过,2项失败,总耗时21.1分钟,共检测了13项测试规范。

在第一次测试之后,所有通过的测试规范都是因为Passmark用通俗易懂的语言指出了我的代码为何与页面内容不符。

以下是第一次测试中的三个例子:

对于dataset-detail.spec.ts这个文件,我原本假设“页面会展示API使用示例(无论是通过curl还是JavaScript编写的)”,但实际上该页面使用的是Python语言和requests库,因此Passmark给出了如下反馈:

“页面上确实包含了API使用示例,但这些示例都是针对Python语言和使用requests库编写的。并没有提供以curl或JavaScript格式呈现的示例。”

虽然页面上确实有相关示例,但我的测试要求是错误的。修改后的要求应该是:接受任何语言格式的示例。

对于dashboard-population.spec.ts这个文件,我原本认为“会生成一张按年龄或种族划分的人口统计图表”,但Passmark指出:

“当前页面展示了诸如活产人数、死亡人数以及人口自然增长情况等关键数据的变化趋势图表,但并没有专门按年龄组或种族来划分人口数据的图表。”

这些图表确实是存在的,只是不是我原本预想的那种形式。解决方法就是:接受任何与人口数据相关的图表即可。

对于kkmnow/hospital-utilisation.spec.ts这个测试用例,我要求提供一个“床位使用率百分比”的指标。Passmark给出的回复是:

“虽然页面后面的表格和排名中确实列出了多个床位使用率百分比数据,但并没有一个显眼的核心指标来显示整体的床位使用率。”

这些数据确实是存在的,只是我的要求与设计师们设计的展示方式不符而已。

这才是最关键的地方:Passmark给出的错误提示并不是堆栈追踪信息,而是对问题原因的说明。人工智能会先读取页面内容,然后与我提出的要求进行对比,从而指出需要修改的地方。这种检测方式完全不同于那些基于选择器的测试工具——后者在遇到问题时往往会抛出TimeoutError: waiting for locator这样的错误信息。

调试循环

一旦我掌握了这个调试流程,它就成为了我最常用的方法。具体的操作步骤如下:

  1. 逐字阅读错误提示信息,不要略读。

  2. 把这条错误提示信息当作对页面内容的描述来相信它。因为人工智能已经读取过页面内容了,而你自己的理解可能并不准确。

  3. 重新表述你的测试需求,使其与页面上的实际内容相匹配。可以适当调整措辞或范围,但必须确保描述是准确的。

  4. 再次运行测试。

关键是要遵守这个规则:不要与测试工具争论。页面就是它本身的样子,而你的错误在于你对页面内容的理解有误。每次当我试图“修正”页面内容、坚称自己的判断是正确的时,其实都在浪费时间;而每当我就错误提示信息本身进行修改后,测试就会顺利通过。

今后我在编写端到端测试用例时,会采取这种做法。反馈循环本身就是一种重要的工具,每一个失败的测试用例实际上都是通往正确答案的起点。

那些仍然失败的两个测试用例才是最值得关注的

1. 这两个模型得出了不同的结果,因此仲裁流程也失败了。

catalogue-search.spec.ts这个测试用例中,Claude认为该测试用例失败(置信度为72%),而Gemini则认为它通过(置信度为100%)。我当初编写测试用例时的表述方式确实存在歧义。

Passmark通过OpenRouter启动了仲裁流程,但Cloudflare返回了504错误代码,导致仲裁程序无法正常进行。最终整个测试用例都失败了。

这种失败情况并非偶然,而是由于系统本身的限制所致。任何使用Passmark进行测试的系统都会受到OpenRouter是否可用的影响。当然,外部网关出现故障也是可能导致这种情况的原因之一。我在最后一次测试中为OpenRouter的调用添加了一个全局重试机制,这样一来,504错误就再也没有出现过。

<如果将这一方案应用于生产环境的持续集成流程中,就需要做好重试机制的准备,并在运行脚本中将 OpenRouter 的故障视为一种需要特别重视的故障类型。>

Playwright HTML报告详细信息,用于说明目录搜索失败的原因。报告显示:Claude和Gemini对同一断言得出了不同的判断结果;Passmark启动了仲裁机制,但仲裁请求因Cloudflare返回504错误而失败。

这次失败让我明白了关于断言设计的一些道理:我的表述不够清晰。Claude的理解是合理的,Gemini的理解也是合理的。当用英语编写测试用例时,准确表达自己的意图是编写高质量测试用例的关键。

2. 等待条件过早被触发。

在KKMNow的测试规范中,我使用了waitUntil: "某个使用指标已经显示出来"这个条件。页面在数字数据尚未完全加载完毕之前,就已经显示出了“医院床位使用率(%)”这一标签。等待机制检测到这个标签后,就认为条件已经满足,从而继续执行后续测试步骤。等到数字数据最终显示出来时,测试时间已经用完了。如果页面能够完全加载完成,那些针对数据范围的断言本来应该是能够通过的。

“页面会显示多个位于指定范围(0%到120%)内的床位使用率数值。例如,排名列表中显示珀里斯州的床位使用率为93.1%,马六甲州的床位使用率为88.2%。”

Playwright HTML报告详细信息,用于说明KKMNow测试规范中的问题。测试在初始的<code>waitUntil</code>步骤中就超时了,但Passmark的注释显示:一旦仪表盘的数据加载完成,那些针对数据范围和状态选择的断言实际上是能够通过的。示例内容为:“珀里斯州的床位使用率为93.1%,马六甲州的床位使用率为88.2%。” height=”400″ loading=”lazy” src=”https://cdn.hashnode.com/uploads/covers/605584805f8d5121697263ca/2a9c8ade-f2ab-4c58-a56b-f7714bcabef5.png” style=”display:block;margin:0 auto” width=”600″/></p>
<p>这个教训告诉我们:<code>waitUntil</code>这个条件的表述同样需要谨慎处理。因为这些条件也是由人工智能来解读的。模糊不清的等待条件与模糊不清的断言一样,都会导致测试失败。</p>
<h2 id=成本是多少,以及为什么缓存率实际上就等于成本率

使用一个工作者对13个测试规范进行三次测试,每次测试大约需要20分钟的时间。黑客马拉松活动中提供的OpenRouter密钥覆盖了所有的人工智能相关费用,因此我并没有具体的金额可以报告。

更具有实际意义的是了解哪些内容会被缓存起来。

$ docker exec passmark-redis redis-cli DBSIZE
5

在三次测试中,大约有35个步骤中的5个步骤被缓存了起来。因此,缓存命中率为14%。Passmark的README文件对此进行了说明:

只有那些只需要执行一次工具调用的步骤才会被缓存。而那些需要执行多个步骤的序列则被认为具有不确定性,因此不会被缓存。

我设计的大多数测试步骤实际上都需要执行多个工具调用。例如“打开区域选择器并选择雪兰莪州,然后等待导航结果显示”这样的步骤,实际上包含了点击、等待和验证三个操作,这些步骤按照设计是不会被缓存的。

这一点对您的预算有着重要影响。如果错误率为86%,那就意味着在每次运行过程中,有86%的步骤都会触发模型的调用。而成本是按照通过OpenRouter进行的每次工具调用来计算的。

要估算自己的费用:首先统计测试用例中那些非原子级的操作步骤,然后将其数量乘以所选模型每次调用的价格(按照当前OpenRouter的费率计算),所得结果就是每次运行该测试用例所需的固定成本。需要注意的是,“缓存填充率”本身也属于成本指标。

解决这个问题的关键在于养成良好的编写规范:将复杂的操作描述拆分成原子级的步骤,并且要重视对“缓存填充率”这一指标的监控,而不能将其视为可以忽略的实施细节。如果一个测试用例中80%的操作都是原子级步骤,那么其成本就会只有那些14%的操作为原子级步骤的测试用例的五分之一。

值得借鉴的这个模式

这个模式的意义其实远超Passmark工具本身。

请确保仪表盘上显示的数字是合理的。大多数团队并没有做到这一点,但它们本应该如此。

像“这个关键指标的值应该在2000万到4000万之间”这样的简单判断语句,能够发现许多常规测试都会遗漏的错误。

以下是四种常见的错误类型:

  • 数据计算时使用了错误的除数,导致屏幕上显示的数字实际上只有实际值的十分之一。

  • 时区设置错误,导致昨天的统计数据被显示在明天的日期下。

  • 数据没有及时更新,用户看到的其实是上周的数据。

  • 地区设置不同导致逗号和小数的位置发生了颠倒,从而使得“1,234,567”这个数字被错误地解读为“1.234567”。

我的目标主要是那些公共信息门户网站;其实,任何显示数字的仪表盘都适用于这种模式——无论是金融科技领域的报告、SaaS分析工具、医疗健康领域的指标数据,还是电子商务平台的管理界面,只要屏幕上显示的数字具有实际意义,这个模式就都能发挥作用。

然而,这些数字中的大多数根本没有人会去进行测试。手工编写检测代码既繁琐又耗时;因此,需要使用特定的选择器来定位这些数字,再编写代码来解析它们、处理单位转换,并计算相关的数值。仅仅为了检测一个数字,就需要编写50行代码……但没人愿意这么做。

其实你并不需要Passmark工具才能借鉴这个思路。在Playwright框架中,使用`page.evaluate`函数和数字解析功能,同样可以编写出有效的检测代码。只不过Passmark版本的代码编写起来更加高效,而且团队中的任何成员都能理解它,而不仅仅是工程师们。

坦率的评价

Passmark确实有效。在进行了三次测试后,我在没有使用任何额外选择器的情况下,从原本只有4个测试用例通过提升到了12个全部通过。

不过,也有一些需要注意的地方:

  • 当缓存处于未初始化状态时,每个操作步骤都需要等待模型计算结果,因此执行这类测试所需的耗时会比使用选择器套件时要长得多。

  • 在我的测试用例中,只有14%的操作步骤被缓存在内存中,其余86%的操作每次运行都会产生模型调用成本。良好的编写规范(确保每个操作都是原子级的)确实能显著影响最终的成本。

  • 即使同时使用了两个模型进行计算,也不能保证它们不会以相同的方式出错;因此,需要编写那些不容易被误解的判断语句。

  • 所有的检测逻辑都依赖于OpenRouter服务的正常运行;如果外部网关出现故障,在自动化测试环境中就需要制定重试策略。

  • 值得强调的是:Passmark并没有让我在使用Playwright框架时变得更熟练,但它确实促使我编写了那些原本会被忽略的测试用例。

    我想,在工作中,我也会采取类似的做法……

    • 每晚针对关键监控界面运行一次Passmark测试,重点检查数据的范围及更新频率。

    • 对于那些需要快速且结果具有确定性的测试任务,仍然继续使用Playwright和Jest工具。

    • 将所有Passmark测试中出现的失败信息视为对页面功能要求的说明,而不是可以争论的错误。

    试试看吧,即使你从未使用过Passmark也好。在你经常使用的某个系统中选择一个数值,编写一个测试脚本:如果这个数值超出了合理的范围,那么测试就会失败。观察一下哪些功能会因此出现故障。这就是本文所要说明的全部内容与目的。

    资源

Comments are closed.