Mejores Prácticas
Esta guía tiene como objetivo compartir nuestras mejores prácticas que te ayudarán a escribir pruebas eficientes y resilientes.
Usa selectores resilientes
Usando selectores que son resistentes a cambios en el DOM, tendrás menos o incluso ninguna prueba fallando cuando, por ejemplo, se elimina una clase de un elemento.
Las clases pueden aplicarse a múltiples elementos y deben evitarse si es posible, a menos que deliberadamente quieras obtener todos los elementos con esa clase.
// 👎
await $('.button')
Todos estos selectores deberían devolver un solo elemento.
// 👍
await $('aria/Submit')
await $('[test-id="submit-button"]')
await $('#submit-button')
Nota: Para descubrir todos los selectores posibles que WebdriverIO soporta, consulta nuestra página de Selectores.
Limita la cantidad de consultas de elementos
Cada vez que usas el comando $
o $$
(esto incluye encadenarlos), WebdriverIO intenta localizar el elemento en el DOM. Estas consultas son costosas, por lo que deberías intentar limitarlas tanto como sea posible.
Consulta tres elementos.
// 👎
await $('table').$('tr').$('td')
Consulta solo un elemento.
// 👍
await $('table tr td')
El único momento en que deberías usar encadenamiento es cuando quieres combinar diferentes estrategias de selector. En el ejemplo usamos los Selectores Profundos, que es una estrategia para entrar en el shadow DOM de un elemento.
// 👍
await $('custom-datepicker').$('#calendar').$('aria/Select')
Prefiere localizar un solo elemento en lugar de tomar uno de una lista
No siempre es posible hacer esto, pero usando pseudo-clases CSS como :nth-child puedes hacer coincidir elementos basados en los índices de los elementos en la lista de hijos de sus padres.
Consulta todas las filas de la tabla.
// 👎
await $$('table tr')[15]
Consulta una sola fila de la tabla.
// 👍
await $('table tr:nth-child(15)')
Usa las aserciones incorporadas
No uses aserciones manuales que no esperan automáticamente a que los resultados coincidan, ya que esto causará pruebas inestables.
// 👎
expect(await button.isDisplayed()).toBe(true)
Al usar las aserciones incorporadas, WebdriverIO esperará automáticamente a que el resultado real coincida con el resultado esperado, resultando en pruebas resilientes. Lo logra reintentando automáticamente la aserción hasta que pasa o se agota el tiempo.
// 👍
await expect(button).toBeDisplayed()
Carga perezosa y encadenamiento de promesas
WebdriverIO tiene algunos trucos bajo la manga cuando se trata de escribir código limpio, ya que puede cargar perezosamente el elemento, lo que te permite encadenar tus promesas y reducir la cantidad de await
. Esto también te permite pasar el elemento como un ChainablePromiseElement en lugar de un Element y para un uso más fácil con objetos de página.
Entonces, ¿cuándo tienes que usar await
?
Siempre deberías usar await
con la excepción de los comandos $
y $$
.
// 👎
const div = await $('div')
const button = await div.$('button')
await button.click()
// o
await (await (await $('div')).$('button')).click()
// 👍
const button = $('div').$('button')
await button.click()
// o
await $('div').$('button').click()
No abuses de comandos y aserciones
Cuando usas expect.toBeDisplayed, implícitamente también esperas a que el elemento exista. No hay necesidad de usar los comandos waitForXXX cuando ya tienes una aserción haciendo lo mismo.
// 👎
await button.waitForExist()
await expect(button).toBeDisplayed()
// 👎
await button.waitForDisplayed()
await expect(button).toBeDisplayed()
// 👍
await expect(button).toBeDisplayed()
No es necesario esperar a que un elemento exista o se muestre al interactuar o al afirmar algo como su texto, a menos que el elemento pueda estar explícitamente invisible (opacity: 0 por ejemplo) o pueda estar explícitamente deshabilitado (atributo disabled por ejemplo), en cuyo caso esperar a que el elemento se muestre tiene sentido.
// 👎
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')
Pruebas Dinámicas
Usa variables de entorno para almacenar datos de prueba dinámicos, por ejemplo, credenciales secretas, dentro de tu entorno en lugar de codificarlos directamente en la prueba. Dirígete a la página Parametrizar Pruebas para más información sobre este tema.
Lintea tu código
Usando eslint para lintear tu código, puedes detectar errores temprano, usa nuestras reglas de linting para asegurarte de que algunas de las mejores prácticas siempre se apliquen.
No uses pause
Puede ser tentador usar el comando pause, pero usarlo es una mala idea ya que no es resiliente y solo causará pruebas inestables a largo plazo.
// 👎
await nameInput.setValue('Bob')
await browser.pause(200) // esperar a que el botón de envío se habilite
await submitFormButton.click()
// 👍
await nameInput.setValue('Bob')
await submitFormButton.waitForEnabled()
await submitFormButton.click()
Bucles asíncronos
Cuando tienes algún código asíncrono que quieres repetir, es importante saber que no todos los bucles pueden hacer esto. Por ejemplo, la función forEach de Array no permite callbacks asíncronos como se puede leer en MDN.
Nota: Aún puedes usar estos cuando no necesites que la operación sea síncrona como se muestra en este ejemplo console.log(await $$('h1').map((h1) => h1.getText()))
.
A continuación se muestran algunos ejemplos de lo que esto significa.
Lo siguiente no funcionará ya que no se admiten callbacks asíncronos.
// 👎
const characters = 'this is some example text that should be put in order'
characters.forEach(async (character) => {
await browser.keys(character)
})
Lo siguiente funcionará.
// 👍
const characters = 'this is some example text that should be put in order'
for (const character of characters) {
await browser.keys(character)
}
Mantenlo simple
A veces vemos a nuestros usuarios mapear datos como texto o valores. Esto a menudo no es necesario y suele ser un indicio de código problemático, comprueba los ejemplos a continuación para ver por qué es el caso.
// 👎 demasiado complejo, aserción síncrona, usa las aserciones incorporadas para prevenir pruebas inestables
const headerText = ['Products', 'Prices']
const texts = await $$('th').map(e => e.getText());
expect(texts).toBe(headerText)
// 👎 demasiado complejo
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]);
}
// 👎 encuentra elementos por su texto pero no tiene en cuenta la posición de los elementos
await expect($('th=Products')).toExist();
await expect($('th=Prices')).toExist();
// 👍 usa identificadores únicos (a menudo utilizados para elementos personalizados)
await expect($('[data-testid="Products"]')).toHaveText('Products');
// 👍 nombres de accesibilidad (a menudo utilizados para elementos html nativos)
await expect($('aria/Product Prices')).toHaveText('Prices');
Otra cosa que a veces vemos es que las cosas simples tienen una solución excesivamente complicada.
// 👎
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();
}
}
Ejecutando código en paralelo
Si no te importa el orden en el que se ejecuta algún código, puedes utilizar Promise.all
para acelerar la ejecución.
Nota: Dado que esto hace que el código sea más difícil de leer, podrías abstraerlo usando un objeto de página o una función, aunque también deberías cuestionar si el beneficio en rendimiento vale el costo de legibilidad.
// 👎
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 se abstrae, podría verse algo como lo siguiente, donde la lógica se coloca en un método llamado submitWithDataOf y los datos se obtienen mediante la clase Person.
// 👍
await form.submitData(new Person('bob@webdriver.io'))