遇到的问题
工作中,当试着为包含无头(headless)UI 单选按钮组功能的用户流编写Playwright测试时,我遇到了一个问题。
某个测试偶尔总会失败,原因是没有成功点击到某个单选按钮。
但可操作性验证步骤是成功的,然后Playwrite测试就尝试点击这个单选按钮。在此期间,Playwright没有抛出任何错误。
但整个测试最后失败了,这是因为随后的断言部分依赖该单选按钮的选择状态。
当我检查相关运行追踪信息时,我可以在单选按钮组下面看到红色的验证错误信息,这个错误只有在没有任何选择,并且用户试图进入下一页时才会出现。
试过的不同解决方案
找到问题的根本原因是容易的部分。
如何修复可操作性检查步骤依然发现不了的脆弱(或不稳定)的点击事件?
这个问题不正是我们先一步做可操作性验证的原因吗?
为了让点击那一步工作,我试了下面几种方法:
- 除了getByTestId之外,使用了其他不同的定位策略
- 在单选按钮的可点击区域内寻找不同HTML元素
- 连续点击20次
- 在试着单击之前,等待某个特定的条件,比如正确的页面URL。总体来说就是添加智能waitFor语句,这些语句不会重复已存在的可操作性验证步骤。
然而这些效果都不好,或者根本不起作用。因此,我卷起“LeetCoder”袖子,尝试了自从freeCodeCamp学习算法以来从未使用过的东西:一段时间的循环。
设计while循环
首先,我需要分析用户场景,以确定“点击失败”和“点击成功”如何影响用户界面。这听起来像是常识,但我们一般并不习惯考虑在失败或成功场景中出现的具体UI元素。
然后我就发现,如果我尝试在不点击单选按钮的情况下继续运行,验证错误将会出现在单选组下。如果我成功点击单选按钮,测试将单击表单上的“下一步”按钮,然后导航到新页面。但即使在单击“下一步”按钮之前,我们也会知道这个,因为isChecked()函数会告诉我们是否选择了单选按钮。这个“下一步”按钮就是我所说的“错误触发器”:因为如果单选按钮点击失败,单击“下一步”按钮将“触发”错误信息。有了这些信息,我就能够创建一个条件语句来确定单选按钮单击是成功还是失败:
let buttonWasClicked = await radioButton.isChecked();
if (buttonWasClicked) {
await errorTextTrigger.click();
errorTextIsVisible = await page.getByTestId("ErrorText").isVisible();
}
但这还不够。如果我们在第一次尝试点击时没有成功,我们需要能多次重试此条件。
这就是为什么我选择使用while循环。我可以说“如果我们看到失败的情况,就再试一次”。
如果我看到成功的情况,我也可以退出循环,从而尽可能少地重試次数。
我还必须精心设计这个while循环,使其不会变成无限循环。
所以我设置最多尝试5次。这是确保此测试不再脆弱(或不稳定)所需的最小循环次数。
while (errorTextIsVisible || !buttonWasClicked) {
if (iterationCounter === 5) {
break;
} else {
iterationCounter++;
}
...
但那一步是我在看到循环方案正在起作用后做的最后一件事。
我之所以从无限迭代开始尝试,是因为如果出现无限循环我就会知道循环没有解决问题。
当循环开始成功时,我就把计数器设置为20,然后是10,然后是5。
我把测试又在本地和CI(持续集成)工具中用多个浏览器运行了多次,以确保此解决方案是无懈可击的。
最终解决方案
您会注意到最终解决方案包含一个waitForURL检查。
这是为了给页面足够的时间加载,以确保我们没有试图与错误的页面进行交互。
我们可能并不很需要它,但它是一层额外的保证。
如果此检查失败,我们甚至不会进入循环,这使我们能够更快地获得反馈。
export async function radioButtonGroupClick(
page: Page,
urlToWaitFor: string, // The URL where the radio button is
radioButton: Locator,
errorTextTrigger: Locator // A button you can click that will throw error text if the radio button wasn't clicked
) {
await page.waitForURL(urlToWaitFor);
let iterationCounter = 0;
let errorTextIsVisible = await page.getByTestId("ErrorText").isVisible();
let buttonWasClicked = await radioButton.isChecked();
while (errorTextIsVisible || !buttonWasClicked) {
if (iterationCounter === 5) {
break;
} else {
iterationCounter++;
}
await radioButton.click();
buttonWasClicked = await radioButton.isChecked();
if (buttonWasClicked) {
await errorTextTrigger.click();
errorTextIsVisible = await page.getByTestId("ErrorText").isVisible();
}
}
}
最后一些想法
这不是我第一次遇到与第三方UI库生成的HTML交互的Playwright测试问题。
例如,您不能在React Select组件上使用Playwright的selectOption。
相反,您必须单击下拉框,并通过其标签文本找到一个选项。
虽然使用这些库中的组件能显著地提高开发人员的生产力,但它们可能会因为脆弱(或不稳定)测试而造成另外的生产力损失。
我们应该记下可以追溯到某个库的脆弱(或不稳定)测试。等到足够长的时间后,可能会发现一些规律,并特别指向特定的某一个库。
市场上有很多UI组件的选择。
如果一个UI 组件没有提供无缝的测试体验,我们也许可以把它换成另一个可以做到的组件。
这是QA在团队内“向左移动”的一种实践。
下次如果您有一个脆弱(或不稳定)测试,并且认为这可能与UI实现有关时,请看看该UI组件的代码。
找到导入语句。在某个地方把用于该UI组件的库都记下来。
如果您觉得合适的话,那么可以做一个实验:将有问题的组件替换为不同UI库的同一组件。
这样也许会减少您的团队在脆弱(或不稳定)测试上花费的时间。
更新:我的解决方案实际上有点糟糕
一位名叫Thananjayan Rajasekaran的读者在我LinkedIn(领英)帖子的评论中提出了更合理的替代方案。
那是我完全忘记了Playwright的expect.toPass() 功能!
我禁不住想立即试试。我太好奇了,不知道这是否能解决我的问题。
然后我就试了。瞧,瞧……
await expect(async () => {
await radioButton.click(); // locator defined outside this block
await expect(radioButton).toBeChecked();
}).toPass();
看看,只有简单的4 行代码,伙计。我当时在做什么?
这种expect.toPass() 函数也修复了我正在处理的另一个脆弱(或不稳定)测试。
(对于那个,我设置了一些间隔时间,以决定重试的频率和重试的次数)
无论如何,希望你也会喜欢我之前过度设计的解决方案。
当时开发它的时候很有趣,即使现在我已经把它从代码库中撤下来了。
干杯,Thananjayan!