learning-front

Nivel 6 · Introducción al testing

Matchers: afirmar lo correcto

toBe frente a toEqual (la comparación por referencia vs por estructura que ya mordía en JavaScript), toBeCloseTo para decimales, toThrow para los errores esperados, y el resto del repertorio. Elegir el matcher correcto es la diferencia entre un test que protege y uno que miente.

Hasta ahora solo has usado un matcher: toBe. Es el más simple, pero usarlo para todo es un error. Un test pasa con el matcher equivocado y, aun así, no protege de nada —o peor, falla cuando el código está bien—. Elegir el matcher correcto es lo que separa un test que vigila de uno que miente. Vamos con los que de verdad usarás.

toBe vs toEqual: el error más común#

toBe compara por identidad: usa Object.is, que para objetos y arrays significa “¿son la misma referencia?”. Para primitivos (números, strings, booleanos) es justo lo que quieres: 4 es 4. Pero con objetos te muerde, y es exactamente la trampa que ya viste en el Nivel 2 con valor vs referencia.

Mira el del medio: está en rojo a propósito.

Los dos objetos { rol: "Daño" } tienen el mismo contenido, pero son dos objetos distintos en memoria: toBe pregunta por la referencia y falla. toEqual compara por estructura, campo a campo y en profundidad, así que pasa. La regla práctica: toBe para primitivos, toEqual para objetos y arrays. Casi todos los tests de datos que escribas usarán toEqual.

Decimales: toBeCloseTo#

¿Recuerdas que 0.1 + 0.2 no daba 0.3 por el error de coma flotante? Eso convierte a toBe en una trampa con cualquier número decimal:

El primer test falla no porque el código esté mal, sino porque toBe exige igualdad exacta y 0.1 + 0.2 es 0.30000000000000004. toBeCloseTo compara con una tolerancia: “lo bastante cerca”. Le puedes pasar cuántos decimales exigir —toBeCloseTo(66.67, 2)— para el winrate sin redondear del motor. Siempre que compares un resultado de una división o un cálculo con decimales, toBeCloseTo.

Errores esperados: toThrow#

A veces lo correcto es que el código lance un error (datos inválidos, una división entre cero). Probar eso tiene su truco: la función se pasa envuelta en una flecha.

¿Por qué la flecha? Si escribieras expect(winratePreciso(5, 0)), la función se ejecutaría y lanzaría ahí mismo, antes de que expect pudiera hacer nada: el test petaría en vez de comprobar el error. Envuelta en () => winratePreciso(5, 0), le das a expect una función que él decide cuándo llamar, atrapando lo que lance. Opcionalmente le pasas el texto (o una regex) que el mensaje debe contener. Y .not.toThrow() afirma lo contrario: que con datos válidos no salta.

El resto del repertorio#

Con toBe, toEqual, toBeCloseTo y toThrow cubres la mayoría de los casos. Estos cuatro más los verás a menudo, cada uno con su ejemplo:

typescript
// toHaveLength: el tamaño de un array o string. Más claro que .length + toBe.
expect(filtrarPorRol(heroes, "Tanque")).toHaveLength(1);

// toContain: ¿está este PRIMITIVO en el array? (compara por identidad).
expect(["Daño", "Tanque", "Apoyo"]).toContain("Tanque");

// toContainEqual: ¿está este OBJETO en el array? (compara por estructura).
// Para arrays de objetos, este; toContain no lo encontraría (otra referencia).
expect(equipo).toContainEqual({ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 });

// toHaveProperty: ¿el objeto tiene esta propiedad? Con .not, que NO la tiene.
expect(heroe).not.toHaveProperty("winrate");

Vitest trae muchos más (toBeTruthy, toBeNull, toMatchObject, toBeGreaterThan…); no hace falta memorizarlos. Cuando necesites uno, lo buscas en su documentación —la tienes en Fuentes—. Lo que importa es el criterio: ¿comparo por referencia o por estructura? ¿exacto o aproximado? ¿espero un valor o un error? Esa pregunta elige el matcher.

Comprueba lo que sabes#

Pregunta 1 de 5

Tienes expect(enriquecer(heroes)[0]) y quieres comprobar que es igual a { nombre: "Genji", winrate: 60, ... }. ¿Qué matcher usas?

Tu turno#

Completa la suite del motor eligiendo el matcher adecuado para cada caso: estructura con toEqual, decimales con toBeCloseTo, errores con toThrow, tamaños con toHaveLength. 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

El matcher correcto para cada caso

Tienes motor.ts (enriquecer, filtrarPorRol y winratePreciso, que lanza sin partidas) y un motor.test.ts con un ejemplo y dos TODO. Completa la suite eligiendo el matcher adecuado: estructura con toEqual, decimales con toBeCloseTo, errores con toThrow, tamaños con toHaveLength.

Paso 1: El matcher correcto en los dos casos clave

  • Pruebas winratePreciso(2, 3) con toBeCloseTo (no con toBe: el decimal no es exacto).
  • Pruebas que winratePreciso(5, 0) lanza, con expect(() => ...).toThrow().
  • Todo pasa en verde.
Ver soluciones
// motor.test.ts — tier OK: el objeto con toEqual y el decimal con toBeCloseTo.
import { describe, it, expect } from "vitest";
import { enriquecer, winratePreciso, type Heroe } from "./motor";

describe("enriquecer", () => {
  it("añade el winrate al héroe (objeto: toEqual, no toBe)", () => {
    const heroes: Heroe[] = [
      { nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
    ];
    // toEqual compara campo a campo; toBe fallaría (es otra referencia).
    expect(enriquecer(heroes)[0]).toEqual({
      nombre: "Genji",
      rol: "Daño",
      partidas: 10,
      victorias: 6,
      winrate: 60,
    });
  });
});

describe("winratePreciso", () => {
  it("calcula el ratio sin redondear", () => {
    // 2 de 3 = 66.666...: toBe(66.67) fallaría; toBeCloseTo tolera la imprecisión.
    expect(winratePreciso(2, 3)).toBeCloseTo(66.67, 2);
  });
});

Por qué este nivel

  • Los dos casos donde el matcher importa de verdad: un decimal inexacto (toBeCloseTo) y un error esperado (toThrow). Con eso ya demuestras que entiendes la elección.
  • El objeto se compara con toEqual: si hubieras usado toBe, el test fallaría aunque el resultado fuese correcto, porque es otra referencia.