En la Parte 1 vimos partición de equivalencia y valores límite — las técnicas que te dicen qué valores probar.
Hoy subimos de nivel. Tablas de decisión y transición de estados te dicen qué combinaciones probar y qué caminos puede tomar tu sistema. Son las técnicas que separan al QA que ejecuta casos de prueba del QA que los diseña.
Esta es la Parte 2 de 3. Si no leíste la Parte 1 (partición de equivalencia y valores límite), te recomiendo empezar por ahí — los conceptos se construyen uno sobre otro. En la Parte 3 aplicaremos las cuatro técnicas juntas en un caso práctico E2E completo.
Primero: la solución del ejercicio de la Parte 1
En la Parte 1 te pedí que diseñaras las particiones y valores límite para un formulario de registro con 4 campos. Aquí va la solución:
Particiones
| Campo | Particiones válidas | Particiones inválidas |
|---|---|---|
Con @ y dominio (user@mail.com) | Sin @ (usermail), sin dominio (user@), vacío | |
| Contraseña | 8 a 64 caracteres | Menos de 8, más de 64, vacía |
| Edad | 13 a 120 | Menor a 13, mayor a 120, 0, negativo |
| Código postal | 5 dígitos (12345) | Menos de 5, más de 5, con letras (1234A), vacío |
Valores límite
| Campo | Bajo mín. | Mínimo | Máximo | Sobre máx. |
|---|---|---|---|---|
| Contraseña (longitud) | 7 chars | 8 chars | 64 chars | 65 chars |
| Edad | 12 | 13 | 120 | 121 |
| Código postal (longitud) | 4 dígitos | 5 dígitos | 5 dígitos | 6 dígitos |
Los datos de prueba diseñados
// Diseño universal — funciona con cualquier frameworkconst registroTests = { email: { válidos: ["usuario@correo.com", "a@b.co"], inválidos: [ { valor: "sinArroba", error: "Email inválido" }, { valor: "sin@dominio", error: "Email inválido" }, { valor: "", error: "El email es obligatorio" }, ], }, contraseña: { válidos: ["A".repeat(8), "A".repeat(64)], // límites inválidos: [ { valor: "A".repeat(7), error: "Mínimo 8 caracteres" }, // bajo mínimo { valor: "A".repeat(65), error: "Máximo 64 caracteres" }, // sobre máximo ], }, edad: { válidos: [13, 120], // límites inválidos: [ { valor: 12, error: "Debes tener al menos 13 años" }, // bajo mínimo { valor: 121, error: "Edad no válida" }, // sobre máximo { valor: -1, error: "Edad no válida" }, // negativo ], }, códigoPostal: { válidos: ["12345"], inválidos: [ { valor: "1234", error: "Debe tener 5 dígitos" }, { valor: "123456", error: "Debe tener 5 dígitos" }, { valor: "1234A", error: "Solo dígitos numéricos" }, ], },};Si tu diseño se parece a esto, vas por buen camino. Si te faltó alguna partición inválida — eso es exactamente lo que estas técnicas previenen.
Tabla de decisión (Decision Table Testing)
Qué dice el syllabus (en 3 líneas)
Cuando el comportamiento del sistema depende de combinaciones de condiciones, una tabla de decisión mapea todas las combinaciones posibles y el resultado esperado de cada una. Cada columna es una regla. Cada fila es una condición o una acción.
El bug que lo explica
E-commerce. Reglas de descuento:
- Los empleados tienen 20% de descuento
- Los cupones dan 15% de descuento
- Los miembros premium tienen envío gratis
Cada regla por separado funcionaba perfecto. El equipo probó empleado con descuento, cupón con descuento, premium con envío gratis. Todo verde.
Nadie probó: empleado + cupón + premium. ¿Se acumulan los descuentos? ¿El 20% aplica sobre el precio original o sobre el precio ya rebajado por el cupón?
Un cliente interno compró un monitor de $500 con las tres condiciones activas. El sistema aplicó 20% + 15% = 35% de descuento, más envío gratis. Pagó $325. El precio correcto era $340 (20% sobre el original, y el cupón no era acumulable con descuento de empleado).
$15 de diferencia por compra. Multiplicado por 200 empleados comprando en el Black Friday.
El bug no estaba en ninguna regla individual. Estaba en la combinación.
La tabla de decisión
Las condiciones son binarias (sí/no). Con 3 condiciones, hay 2³ = 8 combinaciones posibles:
| Regla | Empleado | Cupón | Premium | Descuento | Envío gratis |
|---|---|---|---|---|---|
| R1 | No | No | No | 0% | No |
| R2 | Sí | No | No | 20% | No |
| R3 | No | Sí | No | 15% | No |
| R4 | No | No | Sí | 0% | Sí |
| R5 | Sí | Sí | No | 20% (cupón no acumulable) | No |
| R6 | Sí | No | Sí | 20% | Sí |
| R7 | No | Sí | Sí | 15% | Sí |
| R8 | Sí | Sí | Sí | 20% (cupón no acumulable) | Sí |
8 combinaciones. 8 tests. Cada uno cubre un escenario que el sistema debe manejar correctamente. Sin la tabla, R5 y R8 nunca se prueban — y ahí es donde viven los bugs.
Los bugs más caros no están en las reglas. Están en las combinaciones que nadie probó.
El diseño de los datos
// Tabla de decisión — diseño de pruebas independiente de herramientaconst reglasDescuento = [ // Empleado Cupón Premium → Descuento esperado Envío gratis { empleado: false, cupón: false, premium: false, descuento: "0%", envíoGratis: false, }, { empleado: true, cupón: false, premium: false, descuento: "20%", envíoGratis: false, }, { empleado: false, cupón: true, premium: false, descuento: "15%", envíoGratis: false, }, { empleado: false, cupón: false, premium: true, descuento: "0%", envíoGratis: true, }, { empleado: true, cupón: true, premium: false, descuento: "20%", envíoGratis: false, }, { empleado: true, cupón: false, premium: true, descuento: "20%", envíoGratis: true, }, { empleado: false, cupón: true, premium: true, descuento: "15%", envíoGratis: true, }, { empleado: true, cupón: true, premium: true, descuento: "20%", envíoGratis: true, },];Fíjate: cada objeto es una fila de la tabla. Las propiedades booleanas son las condiciones. Los valores esperados son las acciones. Este diseño es el test. El framework solo lo ejecuta.
En código (ejemplo con Playwright)
import { test, expect } from "@playwright/test";
test.describe( "Descuentos — tabla de decisión", { tag: "@istqb", }, () => { reglasDescuento.forEach((regla, i) => { const nombre = [ regla.empleado ? "empleado" : null, regla.cupón ? "cupón" : null, regla.premium ? "premium" : null, ] .filter(Boolean) .join(" + ") || "sin condiciones";
test(`R${i + 1}: ${nombre} → ${regla.descuento} desc, envío ${regla.envíoGratis ? "gratis" : "normal"}`, async ({ page, }) => { await page.goto("/checkout");
// Configurar condiciones if (regla.empleado) { await page.getByLabel("Soy empleado").check(); } if (regla.cupón) { await page.getByLabel("Código cupón").fill("DESCUENTO15"); await page.getByRole("button", { name: "Aplicar" }).click(); } if (regla.premium) { // Asumimos que el usuario premium ya está logueado await expect(page.getByTestId("badge-premium")).toBeVisible(); }
// Verificar resultados await expect(page.getByTestId("descuento")).toHaveText(regla.descuento);
if (regla.envíoGratis) { await expect(page.getByText("Envío gratis")).toBeVisible(); } else { await expect(page.getByText("Envío gratis")).not.toBeVisible(); } }); }); },);Con un solo forEach sobre el array de reglas, tienes 8 tests que cubren todas las combinaciones. Cambia el array y los tests se adaptan automáticamente. Cambia de framework y solo reescribes las líneas de interacción.
Lo que el examen te va a preguntar
Pregunta típica: “Un sistema tiene 4 condiciones binarias. ¿Cuántas reglas tiene la tabla de decisión completa?” Respuesta: 2⁴ = 16.
El syllabus también habla de tablas reducidas: si ciertas combinaciones producen el mismo resultado, puedes agruparlas. Por ejemplo, si “premium = sí” siempre da envío gratis sin importar las otras condiciones, puedes usar un guión (—) en las columnas irrelevantes. En el examen, esto reduce el número de tests necesarios. En la práctica, yo prefiero la tabla completa — los bugs se esconden en las combinaciones que “no deberían importar”.
Transición de estados (State Transition Testing)
Qué dice el syllabus (en 3 líneas)
Cuando un sistema puede estar en diferentes estados y cambia de uno a otro mediante eventos, un diagrama de transición de estados mapea todos los estados posibles, las transiciones válidas, y — lo más importante — las transiciones que no deberían ser posibles.
El bug que lo explica
Sistema de pedidos en un e-commerce. Los estados eran:
El equipo probó el camino feliz: Pendiente → Confirmado → Enviado → Entregado. Funciona perfecto.
También probaron cancelar: Pendiente → Cancelado. Funciona.
Nadie probó: Enviado → Cancelado.
Un cliente canceló un pedido que ya estaba en camino. El sistema lo aceptó, le devolvió el dinero, pero el paquete ya había salido del almacén. El repartidor llegó, entregó el producto, y el cliente se quedó con el producto y con la devolución.
El bug: la transición Enviado → Cancelado no debía existir, pero nadie lo validó.
El diagrama completo
No basta con probar los caminos que sí funcionan. Hay que probar los que no deberían funcionar:
| Estado actual | Evento | Estado esperado | ¿Válido? |
|---|---|---|---|
| Pendiente | Confirmar | Confirmado | Sí |
| Pendiente | Cancelar | Cancelado | Sí |
| Confirmado | Enviar | Enviado | Sí |
| Confirmado | Cancelar | Cancelado | Sí |
| Enviado | Entregar | Entregado | Sí |
| Enviado | Cancelar | — | No (el bug) |
| Entregado | Cancelar | — | No |
| Entregado | Devolver | Devuelto | Sí |
| Cancelado | Confirmar | — | No |
Los tests de transiciones válidas verifican que tu sistema funciona. Los tests de transiciones inválidas verifican que tu sistema no se rompe.
El diseño de los datos
// Mapa de transiciones — diseño independiente de herramientaconst transiciones = { válidas: [ { desde: "pendiente", evento: "confirmar", hasta: "confirmado" }, { desde: "pendiente", evento: "cancelar", hasta: "cancelado" }, { desde: "confirmado", evento: "enviar", hasta: "enviado" }, { desde: "confirmado", evento: "cancelar", hasta: "cancelado" }, { desde: "enviado", evento: "entregar", hasta: "entregado" }, { desde: "entregado", evento: "devolver", hasta: "devuelto" }, ], inválidas: [ { desde: "enviado", evento: "cancelar", error: "No se puede cancelar un pedido enviado", }, { desde: "entregado", evento: "cancelar", error: "No se puede cancelar un pedido entregado", }, { desde: "cancelado", evento: "confirmar", error: "No se puede reactivar un pedido cancelado", }, { desde: "entregado", evento: "enviar", error: "Transición no permitida" }, ],};La estructura es clara: un array de transiciones válidas (lo que sí debe pasar) y otro de inválidas (lo que no debe pasar). Los bugs viven en el segundo array.
En código (ejemplo con Playwright)
import { test, expect } from "@playwright/test";
test.describe( "Pedidos — transiciones de estado", { tag: "@istqb", }, () => { // Transiciones válidas: verificar que el estado cambia correctamente transiciones.válidas.forEach(({ desde, evento, hasta }) => { test(`${desde} → ${evento} → ${hasta}`, async ({ page }) => { // Crear pedido y llevarlo al estado inicial await page.goto("/admin/pedidos/test"); await page.getByTestId("set-estado").selectOption(desde);
// Ejecutar la transición await page.getByRole("button", { name: evento }).click();
// Verificar el nuevo estado await expect(page.getByTestId("estado-actual")).toHaveText(hasta); }); });
// Transiciones inválidas: verificar que el sistema las rechaza transiciones.inválidas.forEach(({ desde, evento, error }) => { test(`${desde} → ${evento} → BLOQUEADO`, async ({ page }) => { await page.goto("/admin/pedidos/test"); await page.getByTestId("set-estado").selectOption(desde);
await page.getByRole("button", { name: evento }).click();
// El estado NO debe cambiar y debe mostrar error await expect(page.getByTestId("estado-actual")).toHaveText(desde); await expect(page.getByText(error)).toBeVisible(); }); }); },);10 tests. 6 válidos, 4 inválidos. Cubres todos los caminos y todos los muros. Y si mañana agregan un nuevo estado (por ejemplo, “En preparación” entre Confirmado y Enviado), solo agregas filas al array — los tests se generan solos.
Lo que el examen te va a preguntar
Diagrama de estados: El examen te puede dar un diagrama y pedirte que identifiques cuántos estados, transiciones o tests necesitas.
Tabla de transición de estados: Te pueden dar una tabla y pedir que identifiques transiciones inválidas o faltantes.
El syllabus menciona 0-switch (probar cada transición individual) y 1-switch (probar secuencias de 2 transiciones: Pendiente → Confirmado → Enviado). Para el examen CTFL, enfócate en 0-switch — es lo que más preguntan. El concepto de 1-switch aparece más en nivel avanzado.
Pregunta típica del examen: “Un sistema tiene 4 estados y 6 transiciones válidas. ¿Cuántos casos de prueba se necesitan para cubrir todas las transiciones válidas?” Respuesta: 6 (uno por transición).
Combinando las 4 técnicas
Después de dos partes, ya tienes las cuatro técnicas de diseño de pruebas del Capítulo 4:
| Técnica | Te dice | Cuándo usarla |
|---|---|---|
| Partición de equivalencia | Qué grupos de valores probar | Campos con rangos o categorías |
| Valores límite | Dónde exactamente probar dentro de cada grupo | Campos numéricos, longitudes, fechas |
| Tabla de decisión | Qué combinaciones de condiciones probar | Reglas de negocio con múltiples condiciones |
| Transición de estados | Qué caminos y bloqueos probar | Flujos con estados (pedidos, usuarios, pagos) |
Ninguna técnica reemplaza a otra — cada una ataca un tipo diferente de problema. El QA que las domina no necesita que le digan qué probar. Lo ve.
Ejercicio: diseña antes de codear
Un sistema de suscripciones tiene:
Estados: Gratuita → Básica → Premium → Cancelada → Suspendida
Reglas de upgrade:
- Gratuita puede pasar a Básica o Premium
- Básica puede pasar a Premium
- Cualquier plan activo puede cancelarse
- Una cuenta Suspendida (por falta de pago) solo puede pasar a Cancelada o reactivarse al plan anterior
- No se puede hacer downgrade (Premium → Básica)
Tu tarea:
- Dibuja el diagrama de transición de estados (en papel, en tu cabeza, o en código)
- Identifica todas las transiciones válidas e inválidas
- Diseña el array de datos de prueba
- Bonus: Si el upgrade a Premium tiene 3 condiciones (tarjeta válida, email verificado, sin deuda), ¿cuántas reglas tiene la tabla de decisión?
En la Parte 3 resolveremos este ejercicio y lo integraremos en un caso práctico E2E completo: un flujo de e-commerce donde las cuatro técnicas trabajan juntas. De las particiones del registro al diagrama de estados del pedido — todo conectado.
Cheat sheet
| Concepto | Fórmula rápida | Pregunta clave |
|---|---|---|
| Tabla de decisión | N condiciones binarias = 2ᴺ reglas | ”¿Qué pasa cuando se combinan estas condiciones?” |
| Tabla reducida | Agrupar reglas con mismo resultado | ”¿Puedo reducir sin perder cobertura?” |
| Transición de estados | Tests = transiciones válidas + inválidas | ”¿Qué pasa si intento una transición prohibida?“ |
| 0-switch | 1 test por transición | ”¿Cada transición individual funciona?” |
Lo que viene
En la Parte 3 todo se conecta. Un caso práctico E2E completo — un flujo de e-commerce de punta a punta — donde aplicamos las cuatro técnicas juntas. Registro (particiones + límites), checkout (tabla de decisión), y seguimiento del pedido (transición de estados). Un test suite completo que puedes adaptar a tu proyecto.
Si no quieres perdértela, suscríbete al newsletter. Llega antes que nadie.