跳至主要內容

最佳實務

本指南旨在分享我們的最佳實務,以協助您撰寫高效能且具彈性的測試。

使用具彈性的選擇器

使用可彈性應對 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')

唯一應該使用串聯的時候是當您想要結合不同的選擇器策略時。在此範例中,我們使用深度選擇器,這是一種進入元素陰影 DOM 的策略。

// 👍
await $('custom-datepicker').$('#calendar').$('aria/Select')

偏好定位單一元素,而不是從清單中取得

並非總是能夠這樣做,但是使用 CSS 偽類別(例如:nth-child),您可以根據元素在其父項的子清單中的索引來比對元素。

查詢所有表格列。

// 👎
await $$('table tr')[15]

查詢單一表格列。

// 👍
await $('table tr:nth-child(15)')

使用內建的斷言

請勿使用不會自動等待結果比對的手動斷言,因為這將導致測試不穩定。

// 👎
expect(await button.isDisplayed()).toBe(true)

透過使用內建的斷言,WebdriverIO 將自動等待實際結果比對預期結果,進而產生具彈性的測試。它會自動重試斷言,直到通過或逾時為止。

// 👍
await expect(button).toBeDisplayed()

延遲載入與承諾鏈

WebdriverIO 在編寫簡潔程式碼方面有一些訣竅,因為它可以延遲載入元素,這可讓您鏈結您的承諾並減少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()

非同步迴圈

當您有一些想要重複的非同步程式碼時,請務必知道並非所有迴圈都可以做到這一點。例如,Array 的 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('bob@webdriver.io')
await age.setValue('50')
await submitFormButton.waitForEnabled()
await submitFormButton.click()

// 👍
await Promise.all([
name.setValue('Bob'),
email.setValue('bob@webdriver.io'),
age.setValue('50'),
])
await submitFormButton.waitForEnabled()
await submitFormButton.click()

如果將其抽象化,它可能看起來像下面的樣子,其中邏輯放在一個名為 submitWithDataOf 的方法中,並且資料由 Person 類別擷取。

// 👍
await form.submitData(new Person('bob@webdriver.io'))

歡迎!請問有什麼可以幫您?

WebdriverIO AI Copilot