End-to-End Testing

End-to-End Testing

Las pruebas end-to-end operan el sistema real tal como lo haría un usuario. Son lentas, escasas e insustituibles: son las únicas pruebas que pueden demostrar que una persona realmente puede completar una tarea.

intermediate15 min readUpdated 16 sept 2026
checkout.spec.ts
ts
// tests/checkout.spec.ts
import { test, expect } from "@playwright/test";

test("a signed-in user can buy a plan", async ({ page }) => {
  await page.goto("/pricing");

  await page.getByRole("button", { name: "Choose Pro" }).click();
  await page.getByLabel("Card number").fill("4242 4242 4242 4242");
  await page.getByRole("button", { name: "Pay" }).click();

  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});
Alcance
Todo el sistema
Velocidad
Segundos por prueba
Cantidad
Pocas y de alto valor
Herramientas comunes
Playwright, Cypress
Se ejecuta en
Un navegador real
Cubre
User journeys críticos

Por que importa

Lo que obtienes con e2e testing

Confianza en todo el flujo

Una prueba e2e atraviesa la UI, la API, la base de datos y regresa, por lo que es la única prueba que puede demostrar que un usuario realmente puede completar una tarea.

Una red de seguridad contra regresiones

Un puñado de pruebas de flujo detectan roturas de integración que las unit tests pasan por alto: un campo renombrado, una redirección rota o una migración faltante.

Documentación ejecutable

Una especificación legible describe el producto en el lenguaje del usuario y, a diferencia de una página de wiki, hace que el build falle cuando queda obsoleta.

La imagen completa

Las tres capas de una prueba e2e

Prepara el entorno, ejecuta el flujo del usuario y verifica lo que una persona vería.

El stack

Arrange

Una base de datos, API y frontend reales, inicializados en un estado conocido. Si el entorno no es reproducible, ninguna aserción es confiable.

El recorrido

Act

Opera la interfaz tal como lo haría una persona, a través de roles, etiquetas y texto visible en lugar de detalles de implementación.

El resultado

Assert

Verifica lo que el usuario ve y, cuando sea relevante, confirma el efecto secundario a través de la API o la base de datos.

HTML5 de un vistazo

El toolkit de e2e

Navegar

page.goto carga una ruta y la prueba comienza desde una URL real.

Localizar

getByRole, getByLabel y getByText encuentran elementos tal como lo haría un usuario.

Actuar

click, fill, type y select operan la interfaz.

Verificar

expect(locator).toBeVisible() reintenta hasta que el resultado sea verdadero.

Autenticar

Reutiliza un storageState guardado en lugar de iniciar sesión en cada prueba.

Observar

Espera respuestas, bloquea terceros y verifica llamadas a la API.

Flujo

El recorrido detrás de cada prueba exitosa

Una buena prueba e2e prepara el mundo, realiza un recorrido y deja el entorno tal como lo encontró.

  1. 1

    Cargar datos a través de la API

    Crea el usuario, el plan y cualquier registro que el recorrido necesite mediante llamadas a la API, no haciendo clic en pantallas de configuración.

  2. 2

    Abrir la aplicación

    Navega a la ruta de entrada con una sesión autenticada ya establecida para que la prueba comience donde comienza el usuario.

  3. 3

    Realizar la acción

    Opera la interfaz mediante localizadores de rol y etiqueta, un paso de usuario a la vez, sin hacer pausas de tiempo arbitrarias.

  4. 4

    Verificar el resultado visible

    Comprueba lo que el usuario vino a ver. Cuando el resultado es un efecto secundario, confírmalo también a través de la API.

  5. 5

    Capturar evidencia en caso de fallo

    Guarda trazas, capturas de pantalla y video para que un build rojo en CI pueda diagnosticarse sin necesidad de una reproducción local.

  6. 6

    Resetear el estado

    Elimina o expira lo que la prueba creó, o ejecuta cada prueba con datos aislados para que el orden nunca importe.

Una breve historia

La evolución del browser testing

  1. 2006

    Llega Selenium

    La automatización del navegador se vuelve posible y el end-to-end testing se convierte en una práctica real con una curva de aprendizaje pronunciada.

    06
  2. 2015

    Se funda Cypress

    Un runner orientado al desarrollador coloca el navegador en el mismo proceso y hace que la depuración sea mucho más sencilla.

    15
  3. 2020

    Se lanza Playwright

    Una librería cross-browser con auto-waiting redefine las expectativas sobre qué tan estables pueden ser las pruebas de navegador.

    20
  4. 2021

    Playwright Test

    Un runner oficial añade fixtures, paralelismo, tracing y sharding a la librería.

    21
  5. Hoy

    Un filtro de calidad enfocado

    Los equipos mantienen suites e2e pequeñas y de alto valor, ejecutándolas en pull requests y antes del lanzamiento.

    Hoy

La guia completa

End-to-End Testing: Todo lo que necesitas saber

Qué significa realmente el testing end-to-end

El testing end-to-end pone en marcha el sistema real desde el punto de vista del usuario. En lugar de llamar a una función o a una ruta de forma aislada, un test e2e abre la aplicación, realiza los mismos pasos que haría una persona y verifica que el resultado que el usuario vería haya ocurrido realmente.

Esta definición tiene dos consecuencias. Primero, el test cruza cada límite del sistema: navegador, código frontend, HTTP, API, base de datos y de vuelta. Segundo, las aserciones se centran en el comportamiento, no en la implementación. Verificas que una confirmación sea visible, no que se haya llamado a una función en particular.

Debido a que todo el stack está involucrado, los tests e2e son lo más parecido a una garantía de que un usuario puede completar una tarea. También son los tests más lentos y costosos que escribirás, razón por la cual debes escribir pocos y elegirlos cuidadosamente.

Dónde encajan los e2e en la pirámide de pruebas

La pirámide de pruebas es una regla general sobre cuántas pruebas de cada tipo se deben escribir. La base es ancha: muchas pruebas unitarias rápidas. El medio es más estrecho: pruebas de integración entre unos pocos componentes. La cima es pequeña: un puñado de flujos end-to-end.

  • Las pruebas unitarias duran milisegundos, se ejecutan sin infraestructura y sirven para fijar la lógica pura y los casos borde.
  • Las pruebas de integración duran segundos, ponen a prueba una ruta o un módulo con colaboradores reales y detectan errores de conexión.
  • Las pruebas end-to-end duran decenas de segundos, se ejecutan contra un stack desplegado y demuestran que un usuario puede completar una tarea crítica.

La forma es importante porque el coste aumenta a medida que subes y el feedback se vuelve más lento. Un bug que una prueba unitaria puede encontrar debe ser detectado por una prueba unitaria. Reserva los e2e para aquellos flujos donde un camino roto significa un producto roto: registro, inicio de sesión, checkout, publicar, invitar. Si una prueba no necesita un navegador para tener sentido, probablemente no debería usar uno.

Una comprobación de coherencia útil: por cada prueba e2e, pregunta qué prueba unitaria o de integración habría detectado el mismo bug. Si la respuesta es “una que es fácil de escribir”, la prueba e2e está en la capa equivocada.

Cómo elegir los flujos que vale la pena automatizar

No puedes automatizar cada camino posible, así que elige aquellos donde el coste de un fallo sea mayor y donde sea más probable que un bug se filtre a través de las capas inferiores.

Califica cada flujo candidato basándote en dos ejes: impacto de negocio y riesgo de integración. El valor real de las pruebas e2e reside donde el impacto y el riesgo son elevados.

  • Alto impacto, alto riesgo — registro, login, checkout, restablecimiento de contraseña. Automatiza estos primero.
  • Alto impacto, bajo riesgo — la página de inicio de marketing. Cúbrela con un smoke test, no con un flujo completo.
  • Bajo impacto, alto riesgo — una exportación de administrador que se usa mensualmente. Un único test es suficiente, o una comprobación mediante script.
  • Bajo impacto, bajo riesgo — interruptores de configuración y estados cosméticos. Déjalos para los tests unitarios y de componentes.

Los flujos que cruzan más fronteras son los más valiosos, porque son aquellos a los que un test unitario no puede llegar. Un checkout interactúa con el carrito, los precios, el pago, el servicio de pedidos y el email; exactamente las conexiones que se rompen silenciosamente cuando un equipo renombra un campo.

Escribe la lista y revísala cada trimestre. El conjunto de flujos críticos cambia a medida que el producto crece, y el flujo importante de ayer puede convertirse en el código muerto de hoy.

Eligiendo una herramienta: Playwright o Cypress

Dos herramientas dominan las pruebas de navegador modernas, y ambas son excelentes.

Playwright controla Chromium, Firefox y WebKit a través de una sola API. Ejecuta las pruebas en procesos worker en paralelo por defecto, soporta múltiples proyectos de navegador, tiene locators con auto-espera e incluye un trace viewer que graba la ejecución fotograma a fotograma. Su storageState simplifica la reutilización de la autenticación, y su fixture request te proporciona un cliente de API en la misma prueba. Es la opción predeterminada más común para nuevas suites de pruebas.

Cypress se ejecuta en el event loop del navegador, lo que hace que la depuración se sienta inmediata: puedes inspeccionar el DOM en cualquier momento y hacer “time-travel” a través de los comandos en el runner. Su API es amigable y sus mensajes de error son excelentes. La contrapartida es que el soporte multiplataforma y el paralelismo han requerido históricamente más configuración.

La elección rara vez importa tanto como la disciplina. Tanto una suite de Playwright bien escrita como una de Cypress funcionan correctamente. Elige una, aprende su modelo de espera y enfoca tu energía en el aislamiento y los locators.

La anatomía de una prueba

Casi todas las pruebas e2e tienen las mismas tres partes, generalmente llamadas arrange, act y assert.

test("a signed-in user can upgrade to Pro", async ({ page }) => {
  // arrange: the account and plan already exist
  // act: perform the journey
  await page.goto("/pricing");
  await page.getByRole("button", { name: "Choose Pro" }).click();

  // assert: the user sees the result
  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});

Mantén el paso de arrange fuera de la UI siempre que sea posible. Crear un usuario haciendo clic en un formulario de registro añade minutos a la suite y acopla cada prueba al flujo de registro. En su lugar, créalo a través de la API y comienza la prueba ya autenticado.

Otro hábito que vale la pena adoptar es realizar un solo recorrido por prueba. Cuando una prueba cubre cinco cosas y falla, solo sabes que una de esas cinco cosas se rompió. Cuando cubre una sola, el nombre de la prueba que falla es el diagnóstico.

Escribir un flujo que se lea bien

Un test e2e se lee con mucha más frecuencia de la que se escribe. El lector debería ser capaz de entender qué funcionalidad protege sin necesidad de abrir la aplicación.

Nombra el test basándote en el resultado para el usuario, no en la implementación. “un usuario autenticado puede mejorar su plan a Pro” es mejor que “test click botón de checkout”. Usa el tiempo presente y el vocabulario del usuario.

Estructura el test como una secuencia de acciones del usuario, una por línea, con líneas en blanco entre las fases.

test("an admin can invite a teammate", async ({ page }) => {
  await page.goto("/team");

  await page.getByRole("button", { name: "Invite" }).click();
  await page.getByLabel("Email").fill("[email protected]");
  await page.getByRole("button", { name: "Send invite" }).click();

  await expect(page.getByText("Invitation sent")).toBeVisible();
});

Extrae los flujos repetidos en helpers, pero mantén el test legible. Un helper llamado upgradeToPro(page) es correcto; un helper que oculte el test completo no lo es. El test debe seguir mostrando los pasos.

Por último, comenta el “por qué”, no el “qué”. El código ya indica qué botón se clickeó; un comentario solo es útil cuando explica por qué existe el test o por qué un paso parece extraño.

Locators: habla con el usuario, no con el DOM

Un locator es la forma en que la prueba encuentra un elemento. La mejora más significativa que puedes hacer en una suite de e2e es elegir los locators de la misma manera que lo haría un usuario.

Playwright los ordena desde el más preferido al menos preferido:

  1. getByRole — botones, enlaces, encabezados e inputs mediante su rol accesible y nombre.
  2. getByLabel — controles de formulario mediante su etiqueta (label).
  3. getByText — texto visible.
  4. getByPlaceholder, getByAltText, getByTitle — otros atributos visibles.
  5. getByTestId — un test id estable cuando ninguna opción orientada al usuario sea viable.
await page.getByRole("button", { name: "Add to cart" }).click();
await page.getByLabel("Email").fill("[email protected]");
await expect(page.getByRole("heading", { name: "Your cart" })).toBeVisible();

Este orden no es una cuestión estética. Los locators de rol y etiqueta fallan cuando un elemento no es accesible, por lo que la prueba sirve también como una verificación de accesibilidad. Además, sobreviven a las refactorizaciones: renombrar una clase de CSS no los rompe, pero sí rompe los .btn.btn--primary > span.

Usa getByTestId de forma deliberada, no por defecto. Un test id es un contrato que debes mantener, y una suite llena de ellos no te dice nada sobre si la interfaz tiene sentido.

Gestión de la autenticación

Iniciar sesión a través de la UI antes de cada prueba es la causa más común de suites de e2e lentas e inestables. Un login implica un formulario, un viaje de ida y vuelta de red y una redirección, repetidos cientos de veces.

La solución es autenticarse una sola vez y reutilizar la sesión. Playwright llama a la sesión guardada storageState: cookies y local storage serializados en un archivo.

// tests/auth.setup.ts
import { test as setup, expect } from "@playwright/test";

setup("authenticate", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill(process.env.E2E_EMAIL!);
  await page.getByLabel("Password").fill(process.env.E2E_PASSWORD!);
  await page.getByRole("button", { name: "Sign in" }).click();

  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
  await page.context().storageState({ path: "playwright/.auth/user.json" });
});

Un proyecto de configuración (setup project) escribe el archivo, y los proyectos del navegador dependen de él y lo cargan a través de use.storageState. Las pruebas que requieran un usuario diferente utilizarán un archivo de estado distinto.

// playwright.config.ts
export default defineConfig({
  projects: [
    { name: "setup", testMatch: /auth\.setup\.ts/ },
    {
      name: "chromium",
      use: { storageState: "playwright/.auth/user.json" },
      dependencies: ["setup"],
    },
  ],
});

Aun así, deberías mantener una prueba que inicie sesión a través de la interfaz. El formulario de login es un flujo de usuario por derecho propio, y el atajo nunca debe ser la única cobertura de este proceso.

Haciendo que los tests sean deterministas

El determinismo lo es todo. Un test que a veces pasa es peor que uno que siempre falla, porque enseña al equipo a ignorar el color rojo.

Tres reglas cubren la mayor parte de esto.

Nunca uses sleep. waitForTimeout(3000) es o demasiado corto y genera inestabilidad (flaky), o demasiado largo y hace que el test sea lento. En su lugar, espera a que se cumpla la condición: await expect(locator).toBeVisible(). Las aserciones orientadas a la web reintentan la operación hasta que pasan o expiran el tiempo límite, que es exactamente lo que necesitas.

Controla la red. Haz mock de terceros y dependencias inestables con page.route, para que un sandbox de pagos lento no haga fallar tu suite de tests. Realiza aserciones sobre las peticiones que hace tu app cuando ese sea el comportamiento que estés probando.

await page.route("**/api/recommendations", (route) =>
  route.fulfill({ json: { items: [] } }),
);

Congela el tiempo y la aleatoriedad. Un test que depende de “hoy” o de un id aleatorio fallará en algún límite. Inyecta un reloj o define semillas (seeds) para los valores que lee el test.

También está el propio navegador: las animaciones, el autofocus y las transiciones crean condiciones de carrera (races) que parecen bugs de la aplicación. Desactiva las animaciones en el entorno de tests siempre que sea posible y prioriza las aserciones sobre el estado final.

Cobertura responsiva y multidispositivo

Un flujo que funciona en Chromium puede fallar en WebKit, y un diseño que funciona en una laptop puede ser inutilizable en un teléfono. La cobertura de navegadores y viewports es una de las pocas cosas que el e2e logra y que ninguna otra herramienta puede hacer.

Playwright convierte esto en un problema de configuración. Define un proyecto por navegador y reutiliza las mismas pruebas.

// playwright.config.ts
export default defineConfig({
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
    { name: "webkit", use: { ...devices["Desktop Safari"] } },
    { name: "mobile", use: { ...devices["iPhone 13"] } },
  ],
});

No todas las pruebas necesitan ejecutarse en todos los navegadores. Ejecuta la suite completa en Chromium para obtener feedback rápido, y ejecuta los flujos críticos en todos los proyectos de forma programada o antes de un lanzamiento. Esto mantiene los pull requests ágiles sin sacrificar la cobertura.

Cuando un bug es específico de un navegador, añade una prueba de regresión en el proyecto afectado. Un comentario que enlace al issue es mucho más valioso que una nota vaga diciendo que “Safari es raro”.

Gestión de datos de prueba

Los datos son el punto donde las suites de e2e se vuelven poco fiables sin que nos demos cuenta. Dos pruebas que creen un usuario llamado [email protected] pasarán si se ejecutan solas, pero fallarán si se ejecutan juntas.

Realiza el seed a través de la API. Es más rápido que usar la UI, falla de forma evidente cuando el seeding se rompe y permite que la prueba se centre en el flujo de usuario en lugar de en la configuración.

const res = await request.post("/api/test/users", {
  data: { email: `ada+${Date.now()}@example.com` },
});
expect(res.ok()).toBeTruthy();
const user = await res.json();

Aísla los datos por prueba o por worker. El uso de emails únicos, registros limitados al tenant o un namespace fresco por worker son opciones válidas. Lo importante es que ninguna pareja de pruebas dependa de la misma fila.

Limpia lo que crees, o haz que la limpieza sea innecesaria limitando los datos a una ejecución de prueba y eliminando todo el scope al finalizar. Los registros residuales se acumulan, ralentizan la base de datos y, eventualmente, provocan fallos que no tienen nada que ver con el cambio actual.

Ejecución contra un stack real

Una prueba e2e necesita un lugar donde ejecutarse: un frontend, una API y una base de datos, todos con la misma versión del código. La opción más reproducible es un stack de docker compose que inicie todo con un solo comando.

docker compose -f docker-compose.e2e.yml up -d --wait
pnpm exec playwright test
docker compose -f docker-compose.e2e.yml down -v

El flag --wait es fundamental: hace que Compose retorne solo cuando los health checks pasen, lo que elimina la condición de carrera de “conexión rechazada en la primera prueba”. El -v al finalizar descarta los volúmenes para que la siguiente ejecución comience desde cero.

Los entornos de preview llevan esto un paso más allá. Una plataforma construye la rama, la despliega en una URL temporal y la expone al job de pruebas. Esto es lo más cercano a producción que puede llegar a estar una ejecución e2e, y permite que los equipos de producto y QA naveguen por la misma build que utilizan las pruebas.

Apunta la suite al entorno mediante una variable de entorno — BASE_URL — y nunca escribas el host directamente en el código (hard-code). La misma suite debería ejecutarse localmente, en CI y contra un preview.

Cuando el flujo es una API, no un navegador

No todos los flujos end-to-end necesitan un navegador. Un webhook, un trabajo en segundo plano, una CLI y un flujo entre servicios son todos end-to-end en el sentido que importa: ponen a prueba el sistema real.

La fixture request de Playwright es un cliente de API completo, por lo que un flujo de API se ve casi idéntico a uno de navegador.

test("a paid order is fulfilled end to end", async ({ request }) => {
  const order = await request.post("/api/orders", {
    data: { sku: "pro-plan", quantity: 1 },
  });
  expect(order.status()).toBe(201);

  await request.post("/api/payments/webhook", {
    data: { orderId: (await order.json()).id, status: "paid" },
  });

  const res = await request.get(`/api/orders/${(await order.json()).id}`);
  expect(await res.json()).toMatchObject({ status: "fulfilled" });
});

Estas pruebas son más rápidas y mucho menos inestables que las pruebas de navegador, así que úsalas para los flujos que genuinamente no necesitan una UI. Para una cobertura de API pura a un nivel más bajo, Supertest es la herramienta más ligera.

Ejecución de e2e en CI

Las pruebas E2E deben ejecutarse en los pull requests y antes de un release, no en cada guardado. En CI, algunos flags marcan la diferencia entre un control útil y uno que termina siendo ignorado.

  • Ejecución headless. No hay pantalla disponible; tanto Playwright como Cypress funcionan por defecto en modo headless en CI.
  • Fragmentación de la suite (sharding). Divide los archivos entre varias máquinas con --shard=1/3 para que el tiempo total de ejecución se mantenga estable a medida que la suite crece.
  • Reintentar una vez. Un único reintento permite distinguir un fallo real de un problema puntual de infraestructura, siempre y cuando monitorees la tasa de reintentos en lugar de permitir que oculten la inestabilidad (flakiness).
  • Subir artefactos en caso de fallo. Los traces, capturas de pantalla y videos son lo que permite diagnosticar un build fallido en CI sin necesidad de reproducirlo localmente.
- run: pnpm exec playwright test --shard=${{ matrix.shard }}/3 --retries=1
- uses: actions/upload-artifact@v4
  if: ${{ !cancelled() }}
  with:
    name: report-${{ matrix.shard }}
    path: playwright-report/

Los reintentos son una red de seguridad, no una solución. Si una prueba necesita un reintento para pasar consistentemente, es que tiene una condición de carrera (race condition) o un problema real de aislamiento; el reintento solo te está dando tiempo para encontrarlo.

Depuración de una ejecución fallida

La primera pregunta después de un fallo siempre es la misma: ¿qué vio exactamente el navegador? Una herramienta que pueda responder a esto sin necesidad de una reproducción local convierte una investigación de dos horas en una de dos minutos.

Playwright graba un trace —capturas de pantalla, snapshots del DOM, red y consola— y el trace viewer reproduce la ejecución paso a paso. Mantén los traces activos en caso de fallo, luego abre el reporte y desplázate hasta el momento exacto en que falló la aserción.

pnpm exec playwright test --trace on-first-retry
pnpm exec playwright show-trace test-results/checkout/trace.zip

Localmente, ejecuta la suite en modo headed o en modo UI para observar la prueba y pausarla. Cypress ofrece la misma idea a través de su runner interactivo, que mantiene un registro de comandos por el cual puedes navegar en el tiempo.

pnpm exec playwright test --ui
pnpm exec playwright test --headed --debug

El hábito que hace que la depuración sea rápida es hacer aserciones sobre lo que realmente está mal. Un fallo en expect(page).toHaveURL(/\/checkout/) te indica que la navegación nunca ocurrió, lo cual es una investigación muy diferente a un fallo en el encabezado de confirmación.

Combatiendo la inestabilidad (flakiness)

La inestabilidad es el impuesto de las pruebas e2e, y casi siempre se debe a una de estas causas:

  • Un await faltante o una condición de carrera (race condition). La prueba actuó antes de que la aplicación se estabilizara. Soluciónalo con una aserción web-first, no con un sleep.
  • Datos compartidos. Dos pruebas modificaron la misma fila. Aísla los datos por prueba o por worker.
  • Una dependencia externa. Un script o API de terceros fue lento. Haz un mock o exclúyelo de la ruta crítica.
  • Un locator frágil. La prueba dependía de un marcado que cambió. Cambia a un locator basado en roles o etiquetas.
  • Una animación. El elemento estaba presente pero aún no era estable. Desactiva las animaciones en el entorno de pruebas.

La forma de solucionar la inestabilidad es tratar cada fallo intermitente como un bug con una causa, ponerlo en cuarentena y corregir el origen. Una suite que llega a verde mediante reintentos es una suite en la que nadie confía, y una suite en la que nadie confía es peor que no tener ninguna suite.

Manteniendo la suite saludable a largo plazo

Una suite de e2e tiene una tendencia natural a crecer hasta volverse lenta y marcar errores. Mantenerla saludable es una práctica continua, no una configuración de una sola vez.

Revisa la suite de la misma manera que revisas el producto. Cuando se elimine una funcionalidad, borra su prueba en el mismo pull request. Una prueba que ya no protege nada es puro coste.

Sigue las métricas que importan: tiempo de ejecución total, tasa de éxito y tasa de reintentos. Un aumento en la tasa de reintentos es la primera señal de que la inestabilidad (flakiness) se está filtrando, mucho antes de que la suite se vuelva visiblemente poco fiable.

Hazte cargo de la suite. Una suite de e2e compartida y sin dueño tiende a degradarse: los fallos se reintentan, las pruebas se omiten y, en cuestión de un trimestre, nadie confía en una ejecución en rojo. Asigna una rotación o un grupo pequeño para mantenerla en verde, y haz que solucionar una prueba inestable sea una tarea prioritaria en lugar de una interrupción.

Mantén la suite lo suficientemente rápida como para ejecutarse en cada pull request. Cuando deje de ser así, utiliza sharding, paralélizala o mueve los flujos menos críticos a una ejecución nocturna. Un quality gate que tarda una hora es un quality gate que la gente intenta evitar.

Qué no cubrir con e2e

La tentación es probar todo a través del navegador porque se siente como el entorno real. Resiste esa tentación. El e2e es la capa más costosa, así que úsala solo donde sea estrictamente necesario.

No cubras con e2e:

  • Lógica pura y casos borde. El formato de fechas, el cálculo de precios o las reglas de validación pertenecen a tests unitarios que se ejecutan en milisegundos.
  • Cada ruta de error. Una página 500 merece un flujo de prueba; las veinte formas en que la API puede fallar son para tests de integración.
  • Combinaciones exhaustivas de inputs. El e2e cubre el camino representativo, no la matriz completa.
  • Comportamiento de componentes. Que un dropdown se abra pertenece a un test de componentes, que es más rápido y preciso.
  • Cualquier cosa ya cubierta en capas inferiores. Duplicar un test unitario en la capa de e2e añade coste y un nuevo modo de fallo sin aportar mayor confianza.

La regla de oro: si el bug se puede encontrar sin un navegador, encuéntralo sin un navegador.

Mejores prácticas

  • Escribe pocos tests e2e y haz que cada uno represente un flujo crítico.
  • Genera los datos de prueba (seed) a través de la API e inicia los tests ya autenticados.
  • Utiliza selectores de rol, etiqueta y texto; deja los test ids como último recurso.
  • Espera a que se cumplan las condiciones con web-first assertions, nunca con waitForTimeout.
  • Aísla los datos por test o por worker para que el orden de ejecución no importe.
  • Mantén un solo flujo por test para que un fallo identifique claramente la causa.
  • Apunta la suite a BASE_URL; ejecútala localmente, en CI y en previews.
  • Captura trazas y video en caso de fallo y súbelos como artefactos de CI.
  • Pon en cuarentena y corrige los flakes; nunca normalices los reintentos como una solución.
  • Mantén al menos un test de login real, incluso cuando reutilices storageState.

Errores comunes

  • Iniciar sesión a través de la UI antes de cada test.
  • Seleccionar elementos mediante clases CSS o la posición en el DOM.
  • Añadir waitForTimeout para “solucionar” una condición de carrera (race condition).
  • Compartir un usuario o registro semilla (seeded) entre varios tests.
  • Ejecutar los tests contra un servidor local iniciado manualmente en lugar de un stack reproducible.
  • Probar lógica de nivel unitario a través del navegador.
  • Hardcodear localhost:3000 para que la suite solo funcione en una máquina.
  • No subir ningún artefacto y, por lo tanto, no poder depurar un fallo en CI.
  • Permitir que un test inestable (flaky) permanezca en estado de error y reintento en lugar de ponerlo en cuarentena.

Próximos pasos

Las pruebas E2E están en la cima de la pirámide y son más efectivas cuando las capas inferiores están sólidas. Lee sobre Supertest para realizar pruebas de API rápidas que cubran la mayor parte de tu superficie HTTP, y Playwright para un recorrido más profundo sobre la herramienta en sí. Si estás evaluando alternativas, Cypress cubre el otro ejecutor principal, y Docker explica cómo levantar el stack reproducible del que depende tu suite de pruebas.

En la practica

Cuatro piezas de una suite e2e real

Un recorrido, un acceso directo de login, datos iniciales y el job de CI que los ejecuta.

tests/checkout.spec.ts
import { test, expect } from "@playwright/test";

test("a signed-in user can upgrade to Pro", async ({ page }) => {
  await page.goto("/pricing");

  await page.getByRole("button", { name: "Choose Pro" }).click();
  await expect(page).toHaveURL(/\/checkout/);

  await page.getByLabel("Card number").fill("4242 4242 4242 4242");
  await page.getByLabel("Expiry").fill("12/30");
  await page.getByLabel("CVC").fill("123");
  await page.getByRole("button", { name: "Pay" }).click();

  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});

Localizadores basados en roles vs selectores CSS

Los localizadores de rol y etiqueta describen lo que el usuario ve y fallan cuando el elemento es inaccesible. Los selectores CSS se rompen en cada refactor y prueban silenciosamente el markup en lugar del comportamiento.

Preferir
await page
  .getByRole("button", { name: "Add to cart" })
  .click();
Evitar
await page
  .locator("div.product > button.btn.btn--primary")
  .click();

Esperar una condición vs sleep

Las aserciones web-first reintentan hasta que la condición se cumple. Un sleep fijo es o demasiado corto y provoca flakiness, o demasiado largo y lento, y oculta la condición de carrera que pretendía solucionar.

Preferir
await expect(
  page.getByText("Order confirmed"),
).toBeVisible();
Evitar
await page.waitForTimeout(5000);
await expect(
  page.getByText("Order confirmed"),
).toBeVisible();

Compromisos

El costo real de las pruebas end-to-end

Las pruebas E2E son las únicas que demuestran que un usuario puede completar una tarea, y las más costosas de mantener. Úsalas con criterio.

Strengths

  • Prueban lo que hacen los usuarios

    Solo una prueba e2e ejercita la UI, la API y la base de datos reales en conjunto, por lo que detecta errores de conexión que ninguna unit test puede ver.

  • Sobreviven a los refactors

    Escritas basándose en roles y texto visible, una prueba de recorrido sigue pasando mientras cambian los componentes, frameworks y APIs internas.

  • Son un lenguaje compartido

    Producto, QA e ingeniería pueden leer la misma especificación y acordar qué significa que algo funcione.

Trade-offs

  • Son lentas y costosas

    Una prueba de navegador toma segundos e infraestructura real, por lo que una suite de cientos convierte cada pull request en un descanso para tomar café.

  • Son inestables por naturaleza

    Tiempos, redes, animaciones y datos compartidos conspiran para hacer fallar las pruebas que no están cuidadosamente aisladas y esperadas.

  • Dan fallos vagos

    Una prueba de recorrido roja te dice que algo en un camino largo se rompió. Identificar qué exactamente requiere trazas y, a menudo, una reproducción local.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender End-to-End Testing?

Nuestro tutorial interactivo te guia a traves de End-to-End Testing paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.