Перейти до основного вмісту

Найкращі практики

Цей посібник має на меті поділитися нашими найкращими практиками, які допоможуть вам писати ефективні та стійкі тести.

Використовуйте стійкі селектори

Використовуючи селектори, які стійкі до змін у DOM, ви отримаєте менше або навіть взагалі не матимете тестів, що не працюють, коли, наприклад, клас видаляється з елемента.

Класи можуть застосовуватися до кількох елементів, і їх слід уникати, якщо це можливо, якщо ви навмисно не хочете отримати всі елементи з цим класом.

// 👎
await $('.button')

Усі ці селектори повинні повертати один елемент.

// 👍
await $('aria/Submit')
await $('[test-id="submit-button"]')
await $('#submit-button')

Примітка: Щоб дізнатися про всі можливі селектори, які підтримує WebdriverIO, перегляньте нашу сторінку Selectors.

Обмежте кількість запитів до елементів

Кожного разу, коли ви використовуєте команду $ або $$ (це включає їх ланцюжок), WebdriverIO намагається знайти елемент у DOM. Ці запити є дорогими, тому вам слід намагатися обмежити їх кількість.

Запитує три елементи.

// 👎
await $('table').$('tr').$('td')

Запитує лише один елемент.

// 👍
await $('table tr td')

Єдиний випадок, коли слід використовувати ланцюжок, — це коли ви хочете поєднати різні стратегії селекторів. У прикладі ми використовуємо Deep Selectors, це стратегія для входу в shadow 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()
// або
await (await (await $('div')).$('button')).click()
// 👍
const button = $('div').$('button')
await button.click()
// або
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')

Динамічні тести

Використовуйте змінні середовища для зберігання динамічних тестових даних, наприклад, секретних облікових даних, у вашому середовищі замість жорсткого кодування їх у тесті. Перейдіть на сторінку Parameterize Tests для отримання додаткової інформації з цієї теми.

Проводьте статичний аналіз коду

Використовуючи eslint для аналізу вашого коду, ви потенційно можете виявити помилки на ранніх етапах. Використовуйте наші правила лінтингу, щоб переконатися, що деякі з найкращих практик завжди застосовуються.

Не використовуйте паузи

Може виникнути спокуса використати команду pause, але її використання — погана ідея, оскільки вона не є стійкою і в довгостроковій перспективі призведе лише до нестабільних тестів.

// 👎
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)
}

Зберігайте простоту

Іноді ми бачимо, як наші користувачі відображають дані, такі як текст або значення. Це часто не потрібно і часто є ознакою поганого коду. Перевірте приклади нижче, чому це так.

// 👎 занадто складно, синхронне твердження, використовуйте вбудовані твердження, щоб запобігти нестабільним тестам
const headerText = ['Products', 'Prices']
const texts = await $$('th').map(e => e.getText());
expect(texts).toBe(headerText)

// 👎 занадто складно
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]);
}

// 👎 знаходить елементи за їхнім текстом, але не враховує положення елементів
await expect($('th=Products')).toExist();
await expect($('th=Prices')).toExist();
// 👍 використовуйте унікальні ідентифікатори (часто використовуються для користувацьких елементів)
await expect($('[data-testid="Products"]')).toHaveText('Products');
// 👍 імена доступності (часто використовуються для власних HTML-елементів)
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'))

Welcome! How can I help?

WebdriverIO AI Copilot