Bonnes Pratiques
Ce guide vise à partager nos bonnes pratiques qui vous aident à écrire des tests performants et robustes.
Utiliser des sélecteurs robustes
En utilisant des sélecteurs qui résistent aux changements dans le DOM, vous aurez moins ou même aucun test qui échoue lorsque, par exemple, une classe est supprimée d'un élément.
Les classes peuvent être appliquées à plusieurs éléments et devraient être évitées si possible, sauf si vous souhaitez délibérément récupérer tous les éléments avec cette classe.
// 👎
await $('.button')
Tous ces sélecteurs devraient retourner un seul élément.
// 👍
await $('aria/Submit')
await $('[test-id="submit-button"]')
await $('#submit-button')
Remarque : Pour découvrir tous les sélecteurs possibles que WebdriverIO prend en charge, consultez notre page Selectors.
Limiter le nombre de requêtes d'éléments
Chaque fois que vous utilisez la commande $
ou $$
(cela inclut leur chaînage), WebdriverIO essaie de localiser l'élément dans le DOM. Ces requêtes sont coûteuses, vous devriez donc essayer de les limiter autant que possible.
Requête trois éléments.
// 👎
await $('table').$('tr').$('td')
Requête un seul élément.
// 👍
await $('table tr td')
Le seul moment où vous devriez utiliser le chaînage est lorsque vous voulez combiner différentes stratégies de sélection. Dans l'exemple, nous utilisons les Deep Selectors, qui est une stratégie pour accéder au shadow DOM d'un élément.
// 👍
await $('custom-datepicker').$('#calendar').$('aria/Select')
Préférer localiser un seul élément plutôt que d'en prendre un dans une liste
Ce n'est pas toujours possible, mais en utilisant des pseudo-classes CSS comme :nth-child, vous pouvez faire correspondre des éléments en fonction des index des éléments dans la liste des enfants de leurs parents.
Requête toutes les lignes du tableau.
// 👎
await $$('table tr')[15]
Requête une seule ligne du tableau.
// 👍
await $('table tr:nth-child(15)')
Utiliser les assertions intégrées
N'utilisez pas d'assertions manuelles qui n'attendent pas automatiquement que les résultats correspondent, car cela entraînera des tests instables.
// 👎
expect(await button.isDisplayed()).toBe(true)
En utilisant les assertions intégrées, WebdriverIO attendra automatiquement que le résultat réel corresponde au résultat attendu, ce qui donne des tests robustes. Il y parvient en réessayant automatiquement l'assertion jusqu'à ce qu'elle réussisse ou expire.
// 👍
await expect(button).toBeDisplayed()
Chargement paresseux et chaînage de promesses
WebdriverIO a quelques astuces en ce qui concerne l'écriture de code propre, car il peut charger paresseusement l'élément, ce qui vous permet de chaîner vos promesses et de réduire la quantité de await
. Cela vous permet également de passer l'élément en tant que ChainablePromiseElement au lieu d'un Element et pour une utilisation plus facile avec les objets de page.
Alors quand devez-vous utiliser await
?
Vous devriez toujours utiliser await
à l'exception des commandes $
et $$
.
// 👎
const div = await $('div')
const button = await div.$('button')
await button.click()
// ou
await (await (await $('div')).$('button')).click()
// 👍
const button = $('div').$('button')
await button.click()
// ou
await $('div').$('button').click()
Ne pas surutiliser les commandes et les assertions
Lorsque vous utilisez expect.toBeDisplayed, vous attendez implicitement que l'élément existe. Il n'est pas nécessaire d'utiliser les commandes waitForXXX lorsque vous avez déjà une assertion qui fait la même chose.
// 👎
await button.waitForExist()
await expect(button).toBeDisplayed()
// 👎
await button.waitForDisplayed()
await expect(button).toBeDisplayed()
// 👍
await expect(button).toBeDisplayed()
Pas besoin d'attendre qu'un élément existe ou soit affiché lors de l'interaction ou lors de l'assertion de quelque chose comme son texte, sauf si l'élément peut explicitement être invisible (opacity: 0 par exemple) ou peut explicitement être désactivé (attribut disabled par exemple), auquel cas attendre que l'élément soit affiché a du sens.
// 👎
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')
Tests dynamiques
Utilisez des variables d'environnement pour stocker des données de test dynamiques, par exemple des identifiants secrets, dans votre environnement plutôt que de les coder en dur dans le test. Rendez-vous sur la page Parameterize Tests pour plus d'informations à ce sujet.
Lintez votre code
En utilisant eslint pour linter votre code, vous pouvez potentiellement détecter les erreurs tôt, utilisez nos règles de linting pour vous assurer que certaines des meilleures pratiques sont toujours appliquées.
Ne pas mettre en pause
Il peut être tentant d'utiliser la commande pause, mais c'est une mauvaise idée car elle n'est pas robuste et ne fera que causer des tests instables à long terme.
// 👎
await nameInput.setValue('Bob')
await browser.pause(200) // attendre que le bouton de soumission soit activé
await submitFormButton.click()
// 👍
await nameInput.setValue('Bob')
await submitFormButton.waitForEnabled()
await submitFormButton.click()
Boucles asynchrones
Lorsque vous avez du code asynchrone que vous souhaitez répéter, il est important de savoir que toutes les boucles ne peuvent pas le faire. Par exemple, la fonction forEach des tableaux ne permet pas les callbacks asynchrones comme on peut le lire sur MDN.
Remarque : Vous pouvez toujours les utiliser lorsque vous n'avez pas besoin que l'opération soit synchrone comme dans cet exemple console.log(await $$('h1').map((h1) => h1.getText()))
.
Voici quelques exemples de ce que cela signifie.
Ce qui suit ne fonctionnera pas car les callbacks asynchrones ne sont pas pris en charge.
// 👎
const characters = 'this is some example text that should be put in order'
characters.forEach(async (character) => {
await browser.keys(character)
})
Ce qui suit fonctionnera.
// 👍
const characters = 'this is some example text that should be put in order'
for (const character of characters) {
await browser.keys(character)
}
Garder les choses simples
Parfois, nous voyons nos utilisateurs mapper des données comme du texte ou des valeurs. Ce n'est souvent pas nécessaire et c'est souvent un signe de code suspect, vérifiez les exemples ci-dessous pour comprendre pourquoi c'est le cas.
// 👎 trop complexe, assertion synchrone, utilisez les assertions intégrées pour éviter les tests instables
const headerText = ['Products', 'Prices']
const texts = await $$('th').map(e => e.getText());
expect(texts).toBe(headerText)
// 👎 trop complexe
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]);
}
// 👎 trouve des éléments par leur texte mais ne tient pas compte de la position des éléments
await expect($('th=Products')).toExist();
await expect($('th=Prices')).toExist();
// 👍 utiliser des identifiants uniques (souvent utilisés pour les éléments personnalisés)
await expect($('[data-testid="Products"]')).toHaveText('Products');
// 👍 noms d'accessibilité (souvent utilisés pour les éléments HTML natifs)
await expect($('aria/Product Prices')).toHaveText('Prices');
Une autre chose que nous voyons parfois est que des choses simples ont une solution trop compliquée.
// 👎
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();
}
}
Exécution de code en parallèle
Si vous ne vous souciez pas de l'ordre dans lequel certains codes sont exécutés, vous pouvez utiliser Promise.all
pour accélérer l'exécution.
Remarque : Comme cela rend le code plus difficile à lire, vous pourriez l'abstraire en utilisant un objet de page ou une fonction, bien que vous devriez également vous demander si le gain de performance vaut le coût de lisibilité.
// 👎
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()
Si abstrait, cela pourrait ressembler à ce qui suit où la logique est placée dans une méthode appelée submitWithDataOf et les données sont récupérées par la classe Person.
// 👍
await form.submitData(new Person('bob@webdriver.io'))