Aprende a estructurar tus pruebas de WebdriverIO para entender mejor los fallos

En el mundo del testing de software, cada vez es más común escuchar frases como “automatiza todo lo que puedas” o “más automatización es igual a más calidad”. Pero la verdad es que automatizar sin un propósito claro puede llevarnos a resultados contrarios: tests frágiles, difíciles de mantener y que no nos dicen realmente qué está fallando.

Uno de los principios que más intento transmitir en mis cursos y charlas es que la automatización debe ayudarnos a entender mejor el sistema, a tener feedback claro, y a facilitar la evolución del producto. Y una forma muy concreta de lograrlo es estructurando correctamente nuestras pruebas: separando la lógica del negocio de los elementos de la UI, y diseñando nuestros tests para que, cuando algo falla, sepamos exactamente qué está fallando.

En este blog te quiero mostrar un ejemplo práctico con WebdriverIO, usando algo muy común: la validación de un formulario de pago. Vamos a ver primero cómo sería el enfoque “todo mezclado”, y luego cómo podemos refactorizarlo para que tenga mucho más valor.

El caso de ejemplo: un formulario de pago

Imagina que tienes este formulario sencillo en tu aplicación:

  • Campo “Nombre en la tarjeta”
  • Campo “Número de tarjeta”
  • Campo “Fecha de expiración”
  • Campo “CVV”
  • Botón “Pagar”
  • Mensaje de confirmación o de error

Nuestro objetivo es automatizar el flujo básico: completar los datos, enviar el formulario, y validar que se muestra el mensaje esperado.

Primer enfoque: todo mezclado

Aquí un ejemplo de cómo podría verse un test “rápido”, mezclando todo en un solo archivo:

describe('Formulario de pago', () => {
it('Debería procesar un pago exitoso', async () => {

await browser.url('/checkout');

const inputCardholderName = await $('#cardholder-name');
const inputCardNumber = await $('#card-number');
const inputExpiryDate = await $('#expiry-date');
const inputCVV = await $('#cvv');
const buttonPay = await $('#pay-button');
const messageSuccess = await $('#success-message');

await inputCardholderName.setValue('Winston Castillo');
await inputCardNumber.setValue('4111111111111111');
await inputExpiryDate.setValue('12/26');
await inputCVV.setValue('123');

await buttonPay.click();

await expect(messageSuccess).toBeDisplayed();
await expect(messageSuccess).toHaveTextContaining('Pago realizado con éxito');
});

});

A simple vista, este test “funciona”. Si lo corres, te dirá si el pago fue exitoso o no. Pero… ¿qué pasa si mañana cambian el ID de uno de los campos? ¿O si la lógica de validación del negocio cambia? ¿O si queremos reutilizar este flujo en otras pruebas?

Problemas de este enfoque

Es rápido de escribir, pero:

  • Si falla, no queda claro si el fallo fue por un cambio en el UI, en los datos, o en la lógica de negocio.
  • No es reutilizable: cada test que quiera simular un pago tendría que repetir todo esto.
  • Si cambian los selectores, tenemos que tocar todos los tests.
  • Es difícil de mantener a largo plazo.

Segundo enfoque: separación de elementos y lógica

Ahora vamos a ver cómo podemos estructurarlo mejor. Para este ejemplo, vamos a usar el patrón Page Object, y además podemos pensar en separar la lógica de negocio en helpers o actions.

Primero, creamos un Page Object para el formulario de pago:

// /pageobjects/checkout.page.js
class CheckoutPage {

get inputCardholderName() { return $('#cardholder-name'); }
get inputCardNumber() { return $('#card-number'); }
get inputExpiryDate() { return $('#expiry-date'); }
get inputCVV() { return $('#cvv'); }
get buttonPay() { return $('#pay-button'); }
get messageSuccess() { return $('#success-message'); }


async open () {
await browser.url('/checkout');
}

async fillCardholderName(name) {
await this.inputCardholderName.setValue(name);
}

async fillCardNumber(number) {
await this.inputCardNumber.setValue(number);
}

async fillExpiryDate(expiry) {
await this.inputExpiryDate.setValue(expiry);
}

async fillCVV(cvv) {
await this.inputCVV.setValue(cvv);
}

async submitPayment() {
await this.buttonPay.click();
}


async getSuccessMessage() {
return await this.messageSuccess.getText();
}
}
module.exports = new Ch
eckoutPage();


Luego, nuestro test quedaría mucho más limpio:

// /test/specs/checkout.e2e.js
const checkoutPage = require('../../pageobjects/checkout.page');

describe('Formulario de pago', () => {

it('Debería procesar un pago exitoso', async () => {

await checkoutPage.open();

await checkoutPage.fillCardholderName('Winston Castillo');
await checkoutPage.fillCardNumber('4111111111111111');
await checkoutPage.fillExpiryDate('12/26');
await checkoutPage.fillCVV('123');

await checkoutPage.submitPayment();

// Aquí usamos aserción nativa de WDIO con expect-webdriverio
await expect(checkoutPage.messageSuccess).toBeDisplayed();
await expect(checkoutPage.messageSuccess).toHaveTextContaining('Pago realizado con éxito');
});

});

¿Qué ganamos con este enfoque?

Claridad: el test ahora expresa claramente qué se está probando, no cómo se hace.

Separación de responsabilidades:

  • Si cambian los elementos del UI, solo actualizamos el Page Object.
  • Si cambia la lógica de negocio (por ejemplo, nuevas reglas de validación), podemos tener helpers específicos para ello.

Reutilización: podemos llamar a completePayment() en múltiples tests (por ejemplo, para pruebas negativas, para pagos fallidos, etc).

Diagnóstico de fallos: si el test falla, podemos saber más fácilmente si fue por un cambio en el UI (porque falló el Page), o porque la lógica de negocio ya no valida igual.


Más allá: separación aún más fina

Incluso podemos ir un paso más allá. En aplicaciones más complejas, es buena práctica separar:

1️⃣ Page Objects → elementos de la UI

2️⃣ Actions o Flows → flujos de negocio (por ejemplo: “realizar un pago válido”, “realizar un pago con tarjeta vencida”, etc)

3️⃣ Tests → las validaciones y expectativas

Esto hace que el código de automatización sea mucho más mantenible y escalable.

Ejemplo de Action:

// /test/specs/checkout.e2e.js
const { performSuccessfulPayment } = require('../../actions/payment.actions');
const checkoutPage = require('../../pag
eobjects/checkout.page’);

describe('Formulario de pago', () => {

it('Debería procesar un pago exitoso', async () => {

await performSuccessfulPayment();

await expect(checkoutPage.messageSuccess).toBeDisplayed();
await expect(checkoutPage.messageSuccess).toHaveTextContaining('Pago realizado con éxito');
});

});


 El test se vuelve aún más claro.

Automatizar con propósito no es solo “hacer que el test pase”. Es pensar en cómo esa automatización puede servir al equipo:

  • ¿Nos ayuda a detectar cambios en el sistema?
  • ¿Nos dice claramente qué ha fallado?
  • ¿Es fácil de mantener cuando el producto evoluciona?
  • ¿Permite crear nuevos tests de manera sencilla?

Separar la lógica del negocio de los elementos de la UI es un paso fundamental en esa dirección. No solo te ahorra tiempo a futuro, sino que te permite tener una automatización que aporta verdadero valor.

Si estás empezando con WebdriverIO, te animo a experimentar con esta estructura. Y si ya tienes tests en producción, vale la pena revisar si podrías mejorar su diseño con este enfoque.

Como siempre digo: menos tests que “pasan”, más tests que ayudan a entender el sistema.

2 Comments

  • Abel Ramirez

    ¡Excelente artículo! Me gustó cómo explicas la importancia de separar la lógica de negocio de los elementos de la UI mediante Page Objects y flujos. La claridad y reutilización que se obtiene facilita el mantenimiento y la identificación de fallos. El ejemplo del formulario de pago es muy ilustrativo. ¡Muchas gracias por compartir!

  • Carlos Urdaneta

    Excelente artículo. Me gustó mucho cómo explicaste la diferencia entre simplemente automatizar y estructurar correctamente las pruebas. Al separar los elementos de la interfaz y la lógica de negocio en WebdriverIO, el test se vuelve más claro, reutilizable y fácil de mantener. Además, ayuda a identificar si el fallo proviene de cambios en la UI o de la lógica. Gracias por compartir este enfoque!

Leave a Reply

Your email address will not be published. Required fields are marked *