learning-front

Nivel 6 · Introducción al testing

Casos límite y tests parametrizados

Pensar los edge cases de forma sistemática (vacío, frontera, negativos, duplicados) y cubrirlos sin repetirte con it.each. Testear también que las cosas fallan como deben: el smart constructor branded del nivel anterior debe lanzar con un id inválido.

Probar el caso feliz demuestra que el código funciona cuando todo va bien. Pero los bugs casi nunca están ahí: viven en los bordes. Este capítulo va de dos cosas: pensar esos bordes de forma sistemática, y cubrirlos sin repetirte con it.each. Y de una tercera que se olvida: testear que las cosas fallan como deben.

Pensar los casos límite#

En vez de improvisar, ten una lista mental que repasar para cada función. Un caso límite es una entrada en el extremo de lo posible:

  • Vacío — una lista [], un string "".
  • Uno solo — un único elemento (muchos bugs aparecen con uno o con cero).
  • Frontera — el mínimo y el máximo: un winrate de 0 (sin victorias) o de 100 (las gana todas).
  • Negativos / inesperados — un número negativo, un valor del tipo equivocado.
  • Duplicados — dos héroes con el mismo nombre, dos winrates iguales.

No todos aplican a cada función, pero repasar la lista te hace pensar en lo que el caso típico nunca toca. Para winrate, las fronteras claras son 0 y 100; para una lista, el vacío y el de un elemento.

it.each: muchos casos, un solo test#

Probar winrate con seis entradas distintas con seis it casi idénticos es repetición que se desincroniza en cuanto cambias algo. it.each resuelve esto: le pasas una tabla (un array de filas) y genera un test por fila, con el mismo cuerpo.

Fíjate en el informe: cada fila sale como su propia línea, con su nombre. Los %i (o %s para texto, %o para un objeto) son huecos que se rellenan, en orden, con los valores de la fila. Añadir un caso nuevo es añadir una fila —no copiar y pegar un test entero—, y si uno falla, ves exactamente cuál, no “el bloque entero”.

Testear que algo falla como debe#

Hasta ahora hemos comprobado que el código hace lo correcto. Pero a veces lo correcto es rechazar una entrada. Recupera el smart constructor branded del Nivel 5: crearHeroeId solo deja existir un id con el formato válido (2-3 letras mayúsculas) y lanza con cualquier otra cosa. Su trabajo es, literalmente, rechazar lo inválido —así que hay que probar que lo hace—.

Una tabla de ids inválidos, cada uno comprobado con toThrow(); y otra de válidos, con .not.toThrow(). El «¿y qué?» es importante: si un día alguien afloja esa validación y crearHeroeId deja de lanzar, un id corrupto se colaría sin un solo error visible, y reventaría más adelante, lejos de la causa. El test de rechazo es lo que mantiene esa puerta cerrada. Probar solo que acepta lo válido deja la mitad del validador sin vigilar.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Cuál de estas es una buena lista mental de casos límite a probar?

Tu turno#

Completa la tabla it.each de winrate con las fronteras, y añade los tests de crearHeroeId (que rechaza lo inválido y acepta lo válido). Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.

Ejercicio · en esta página

Casos límite con it.each, y validar que falla

Tienes winrate (lanza sin partidas) y crearHeroeId (branded, valida el formato). Cubre los casos frontera de winrate con una tabla it.each, y comprueba que crearHeroeId rechaza los ids inválidos (y acepta los válidos) también con it.each.

Paso 1: La tabla con las fronteras

  • Completas el it.each de winrate con los casos frontera: 0 de 8 (→ 0) y 8 de 8 (→ 100).
  • Cada fila usa %i en el nombre para que el informe muestre el caso concreto.
Ver soluciones
// motor.test.ts — tier OK: una tabla de it.each con los casos frontera incluidos.
import { describe, it, expect } from "vitest";
import { winrate } from "./motor";

describe("winrate", () => {
  // Cada fila es un caso: victorias, partidas, winrate esperado.
  // Las dos últimas son las fronteras: sin victorias (0) y ganándolas todas (100).
  it.each([
    [6, 10, 60],
    [2, 8, 25],
    [0, 8, 0],
    [8, 8, 100],
  ])("%i victorias de %i partidas da %i", (victorias, partidas, esperado) => {
    expect(winrate(victorias, partidas)).toBe(esperado);
  });
});

Por qué este nivel

  • Una tabla de cuatro filas, fronteras incluidas, con un solo cuerpo de test. Mucho mejor que cuatro it copiados que se desincronizan en cuanto cambias algo.
  • Su límite: solo prueba winrate. El validador crearHeroeId, cuyo trabajo es rechazar, todavía no se toca.