最佳实践
本指南旨在分享我们的最佳实践,帮助您编写高性能且弹性的测试。
使用弹性选择器
使用对 DOM 中更改具有弹性的选择器,当例如从元素中删除一个类时,您将减少或甚至不会出现测试失败。
类可以应用于多个元素,如果可能,应避免使用,除非您故意想要获取具有该类的所有元素。
// 👎
await $('.button')
所有这些选择器都应返回单个元素。
// 👍
await $('aria/Submit')
await $('[test-id="submit-button"]')
await $('#submit-button')
注意:要了解 WebdriverIO 支持的所有可能的选择器,请查看我们的选择器页面。
限制元素查询的数量
每次使用$
或$$
命令(包括链接它们)时,WebdriverIO 都会尝试在 DOM 中定位元素。这些查询代价很高,因此您应该尽量减少它们。
查询三个元素。
// 👎
await $('table').$('tr').$('td')
仅查询一个元素。
// 👍
await $('table tr td')
您应该使用链接的唯一时间是当您想要组合不同的选择器策略时。在示例中,我们使用了深度选择器,这是一种进入元素的 Shadow DOM 的策略。
// 👍
await $('custom-datepicker').$('#calendar').$('aria/Select')
优先定位单个元素而不是从列表中获取一个元素
这并非总是可行,但使用像:nth-child这样的 CSS 伪类,您可以根据元素在其父元素的子列表中的索引来匹配元素。
查询所有表格行。
// 👎
await $$('table tr')[15]
查询单个表格行。
// 👍
await $('table tr:nth-child(15)')
使用内置断言
不要使用不会自动等待结果匹配的手动断言,因为这会导致测试不稳定。
// 👎
expect(await button.isDisplayed()).toBe(true)
通过使用内置断言,WebdriverIO 将自动等待实际结果与预期结果匹配,从而产生弹性测试。它通过自动重试断言直到通过或超时来实现这一点。
// 👍
await expect(button).toBeDisplayed()
延迟加载和 Promise 链
在编写干净代码方面,WebdriverIO 有它的一些技巧,因为它可以延迟加载元素,允许您链接您的 Promise 并减少 await
的数量。这还允许您将元素作为 ChainablePromiseElement 而不是 Element 传递,以便更轻松地与页面对象一起使用。
那么您什么时候必须使用 await
呢?除了 $
和 $$
命令之外,您应该始终使用 await
。
// 👎
const div = await $('div')
const button = await div.$('button')
await button.click()
// or
await (await (await $('div')).$('button')).click()
// 👍
const button = $('div').$('button')
await button.click()
// or
await $('div').$('button').click()
不要过度使用命令和断言
当使用 expect.toBeDisplayed 时,您还隐式地等待元素存在。当您已经有断言执行相同操作时,无需使用 waitForXXX 命令。
// 👎
await button.waitForExist()
await expect(button).toBeDisplayed()
// 👎
await button.waitForDisplayed()
await expect(button).toBeDisplayed()
// 👍
await expect(button).toBeDisplayed()
在交互或断言某些内容(如文本)时,无需等待元素存在或显示,除非元素可以明确地不可见(例如 opacity: 0)或可以明确地禁用(例如 disabled 属性),在这种情况下,等待元素显示是有意义的。
// 👎
await expect(button).toBeExisting()
await expect(button).toHaveText('Submit')
// 👎
await expect(button).toBeDisplayed()
await expect(button).toHaveText('Submit')
// 👎
await expect(button).toBeDisplayed()
await button.click()
// 👍
await button.click()
// 👍
await expect(button).toHaveText('Submit')
动态测试
使用环境变量来存储动态测试数据,例如机密凭据,在您的环境中,而不是将其硬编码到测试中。有关此主题的更多信息,请访问参数化测试页面。
代码检查
使用 eslint 检查您的代码,您可以潜在地尽早捕获错误,使用我们的代码检查规则确保始终应用一些最佳实践。
不要暂停
使用暂停命令可能很诱人,但使用它是一个坏主意,因为它不具有弹性,从长远来看只会导致测试不稳定。
// 👎
await nameInput.setValue('Bob')
await browser.pause(200) // wait for submit button to enable
await submitFormButton.click()
// 👍
await nameInput.setValue('Bob')
await submitFormButton.waitForEnabled()
await submitFormButton.click()
异步循环
当您有一些要重复的异步代码时,重要的是要知道并非所有循环都可以做到这一点。例如,数组的 forEach 函数不允许异步回调,如MDN上所述。
注意:当您不需要操作同步时,您仍然可以使用它们,如以下示例所示 console.log(await $$('h1').map((h1) => h1.getText()))
。
以下是一些示例说明了这意味着什么。
以下内容将不起作用,因为不支持异步回调。
// 👎
const characters = 'this is some example text that should be put in order'
characters.forEach(async (character) => {
await browser.keys(character)
})
以下内容将起作用。
// 👍
const characters = 'this is some example text that should be put in order'
for (const character of characters) {
await browser.keys(character)
}
保持简单
有时我们会看到我们的用户映射数据(如文本或值)。这通常不需要,并且通常是代码异味,请查看以下示例了解原因。
// 👎 too complex, synchronous assertion, use the built-in assertions to prevent flaky tests
const headerText = ['Products', 'Prices']
const texts = await $$('th').map(e => e.getText());
expect(texts).toBe(headerText)
// 👎 too complex
const headerText = ['Products', 'Prices']
const columns = await $$('th');
await expect(columns).toBeElementsArrayOfSize(2);
for (let i = 0; i < columns.length; i++) {
await expect(columns[i]).toHaveText(headerText[i]);
}
// 👎 finds elements by their text but does not take into account the position of the elements
await expect($('th=Products')).toExist();
await expect($('th=Prices')).toExist();
// 👍 use unique identifiers (often used for custom elements)
await expect($('[data-testid="Products"]')).toHaveText('Products');
// 👍 accessibility names (often used for native html elements)
await expect($('aria/Product Prices')).toHaveText('Prices');
我们有时还会看到,简单的事情有一个过于复杂的解决方案。
// 👎
class BadExample {
public async selectOptionByValue(value: string) {
await $('select').click();
await $$('option')
.map(async function (element) {
const hasValue = (await element.getValue()) === value;
if (hasValue) {
await $(element).click();
}
return hasValue;
});
}
public async selectOptionByText(text: string) {
await $('select').click();
await $$('option')
.map(async function (element) {
const hasText = (await element.getText()) === text;
if (hasText) {
await $(element).click();
}
return hasText;
});
}
}
// 👍
class BetterExample {
public async selectOptionByValue(value: string) {
await $('select').click();
await $(`option[value=${value}]`).click();
}
public async selectOptionByText(text: string) {
await $('select').click();
await $(`option=${text}]`).click();
}
}
并行执行代码
如果您不关心某些代码的执行顺序,您可以利用Promise.all
来加快执行速度。
注意:由于这会使代码更难阅读,因此您可以使用页面对象或函数将其抽象出来,尽管您也应该质疑性能上的好处是否值得可读性的代价。
// 👎
await name.setValue('Bob')
await email.setValue('[email protected]')
await age.setValue('50')
await submitFormButton.waitForEnabled()
await submitFormButton.click()
// 👍
await Promise.all([
name.setValue('Bob'),
email.setValue('[email protected]'),
age.setValue('50'),
])
await submitFormButton.waitForEnabled()
await submitFormButton.click()
如果抽象出来,它可能看起来像下面这样,其中逻辑放在名为 submitWithDataOf 的方法中,数据由 Person 类检索。
// 👍
await form.submitData(new Person('[email protected]'))