En el Nivel 6 probaste funciones puras: les dabas una entrada y comprobabas su salida. Un componente de React no tiene una “salida” tan limpia: lo que produce es una interfaz —algo que se ve y con lo que se interactúa—. Así que la pregunta cambia de “¿qué devuelve?” a “¿qué ve y qué puede hacer una persona con este componente?”. Esa es toda la idea de Testing Library.
El principio es el mismo que ya conoces: comportamiento sobre implementación. No vamos a mirar el estado interno de React ni qué hook usa el componente por dentro. Vamos a montarlo, mirar la pantalla y actuar sobre ella como lo haría un usuario. Cuanto más se parezca el test a ese uso real, más confianza te da.
render y screen: montar el componente y mirarlo#
Dos piezas hacen casi todo. render(...) monta el componente en un DOM de prueba, igual que el
navegador lo pintaría. screen es tu ventana a ese DOM: sus queries buscan elementos como los
buscaría una persona mirando la pantalla.
screen.getByText("...") busca un elemento por su texto. Si lo encuentra, lo devuelve; si no, no
devuelve null: lanza un error y el test falla con un mensaje que te dice qué buscaba. Esa es una
diferencia clave con querySelector, que devolvía null y te dejaba el fallo para más tarde.
El toBeInTheDocument() del final es un matcher de jest-dom (ahora lo vemos): afirma que el
elemento está de verdad en la página. Lee como una frase.
getByRole primero: buscar como una persona#
Testing Library tiene varias queries, pero hay un orden recomendado, y la primera es getByRole:
buscar por el rol accesible del elemento. El rol es lo que un lector de pantalla anuncia: un
<button> es button, un <h1>–<h6> es heading, un <input type="text"> es textbox.
El segundo argumento, { name: "..." }, filtra por el nombre accesible del elemento: el texto de
un botón, el contenido de un encabezado, la etiqueta de un campo.
¿Y por qué el rol primero, y no un data-testid? Porque un getByRole("button") que pasa te
dice algo que importa: que ese elemento es alcanzable por teclado y por un lector de pantalla. Si
alguien convirtiera el botón en un <div onClick> —que parece un botón pero no tiene rol, ni foco,
ni respuesta al teclado—, getByRole("button") dejaría de encontrarlo y el test fallaría,
avisándote de que has roto la accesibilidad. Un getByTestId("boton-ver"), en cambio, pasaría igual:
solo mira un atributo que el usuario no ve. Por eso el testid es el último recurso, para cuando
de verdad no hay forma accesible de apuntar a un elemento.
Las otras queries que más usarás, cada una con su caso:
// getByRole: por rol accesible. El name es el texto del botón. (La recomendada.)
screen.getByRole("button", { name: "Registrar victoria" });
// getByText: por el texto visible, cuando el rol no aplica (un párrafo, un span suelto).
screen.getByText("Victorias: 0");
// getByLabelText: un campo de formulario por el texto de su <label>. Imita cómo lo
// encuentra una persona: lee la etiqueta, no el id interno del input.
// <label htmlFor="buscar">Buscar héroe</label><input id="buscar" />
screen.getByLabelText("Buscar héroe");Cada una existe para un caso distinto: getByRole para elementos con rol (botones, encabezados,
campos), getByText para texto sin rol propio, getByLabelText para entradas de formulario por su
etiqueta. No son intercambiables: elegir la query correcta es parte de escribir un buen test.
Los matchers de jest-dom#
expect(elemento) te trae los matchers de siempre (toBe, toEqual…), pero para el DOM se quedan
cortos. jest-dom añade matchers pensados para elementos, que leen como una frase y dan mensajes de
error claros:
// ¿Está el elemento en la página?
expect(boton).toBeInTheDocument();
// ¿Su texto es el esperado?
expect(parrafo).toHaveTextContent("Victorias: 2");
// ¿El botón está deshabilitado?
expect(boton).toBeDisabled();
// ¿Tiene este atributo con este valor? (útil para aria-*)
expect(boton).toHaveAttribute("aria-pressed", "true");Compáralo con la alternativa sin jest-dom: expect(boton).not.toBe(null) para saber si existe, o
expect(boton.getAttribute("aria-pressed")).toBe("true") a mano. ¿Y qué ganas? Legibilidad y, sobre
todo, mejores mensajes: cuando toHaveTextContent falla, te dice qué texto había de verdad en el
elemento; un toBe te diría solo “esperaba X, recibí Y” sin contexto. En este curso, jest-dom va
pre-cableado (como un fichero de setup que se carga una vez): no lo importas, ya está disponible
en expect.
user-event frente a fireEvent: simular al usuario#
Probar lo que se ve está bien, pero un componente también reacciona. Para simular la interacción hay dos herramientas, y la diferencia entre ellas es importante.
fireEvent.click(boton) dispara un evento sintético: el click, y nada más. user-event, en
cambio, reproduce la secuencia completa de lo que hace una persona al pulsar: mover el puntero
encima, enfocar el botón, presionar, soltar y, entonces sí, el clic.
¿Y qué más da, si al final el botón recibe su clic? Da mucho, y este es el corazón del asunto: un
componente que abre un menú al mousedown, o un botón que solo actúa cuando tiene el foco,
pasaría un test con fireEvent.click estando roto para una persona real —porque fireEvent
nunca disparó el mousedown ni el focus—. user-event sí los dispara, así que tu test falla cuando
el componente está roto de verdad. Por eso user-event es el estándar de 2026 para probar la
interacción, y fireEvent queda para casos de bajo nivel muy concretos.
Hay un detalle de sintaxis: user-event es asíncrono. Cada acción devuelve una promesa, así que se
usa con await y el test es async. Al esperar, le das tiempo a React a procesar el evento y
repintar antes de que afirmes sobre el resultado. Sin el await, tu expect correría sobre la
pantalla vieja y el test fallaría en falso (o pasaría por casualidad).
Pruébalo#
Toca el Marcador de arriba: cambia el número de clics esperado a propósito (pon "Victorias: 3") y
mira cómo el test pasa a rojo con un mensaje que te dice qué texto había de verdad. Luego
arréglalo. Esa es la sensación que buscas: el test te dice, sin ambigüedad, qué esperaba y qué
encontró.
Comprueba lo que sabes#
Pregunta 1 de 5
Quieres comprobar que existe el botón "Registrar victoria". ¿Por qué se prefiere getByRole("button", { name: "Registrar victoria" }) antes que getByTestId("boton-victoria")?
Tu turno#
Escribe los tests de la TarjetaHeroe. No toques el componente: pruébalo como lo usa una persona.
Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un tier al
siguiente: del caso feliz a la interacción, y de la interacción al comportamiento accesible completo.
Ejercicio · en esta página
Prueba la TarjetaHeroe como la usa una persona
Tienes la TarjetaHeroe del Team Builder: muestra nombre, rol y winrate, y tiene un botón para marcarla como favorita. No tocas el componente: escribe sus tests con Testing Library. Busca por rol accesible (getByRole), afirma con los matchers de jest-dom y simula la interacción con user-event.
Paso 1: Lo que muestra la ficha
- Renderizas la ficha y compruebas que aparece el nombre del héroe como encabezado (getByRole "heading").
- Compruebas también el rol y el winrate calculado.
- Afirmas con un matcher de jest-dom (toBeInTheDocument), no con comprobaciones manuales.
Paso 2: La interacción del botón
- Además del caso feliz, pruebas que al pulsar el botón con user-event la ficha REACCIONA: su etiqueta cambia.
- Buscas el botón por su rol y su nombre accesible.
- El test de interacción es async y usa await en el clic.
Paso 3: El toggle completo, por su comportamiento accesible
- Pruebas el toggle de IDA Y VUELTA: marcar y desmarcar (que no se quede pegado).
- Afirmas sobre aria-pressed con toHaveAttribute y sobre el texto con toHaveTextContent: el estado COMO LO PERCIBE el usuario, no un detalle interno de React.
- Separas los tests por lo que prueban (lo que muestra / el botón), de modo que la salida se lea como documentación del componente.
Ver soluciones
// Tier OK: el caso feliz. La ficha, recién montada, muestra los datos del héroe.
import { describe, it, expect } from "vitest";
// render monta el componente en un DOM de prueba; screen consulta ese DOM.
import { render, screen } from "@testing-library/react";
// El componente bajo prueba y su tipo.
import { TarjetaHeroe, type Heroe } from "./TarjetaHeroe";
// Un héroe de ejemplo, tipado: 13 de 20 partidas → 65% de winrate.
const genji: Heroe = { nombre: "Genji", rol: "Daño", partidas: 20, victorias: 13 };
describe("TarjetaHeroe", () => {
it("muestra los datos del héroe", () => {
// Monta la ficha en el DOM de prueba.
render(<TarjetaHeroe heroe={genji} />);
// El nombre lo buscamos por su ROL de encabezado, no por texto suelto: así el test
// además verifica que "Genji" es un encabezado de verdad (un h1-h6 accesible).
expect(screen.getByRole("heading", { name: "Genji" })).toBeInTheDocument();
// El rol aparece en el texto de la ficha.
expect(screen.getByText("Rol: Daño")).toBeInTheDocument();
// 13/20 = 65%: comprobamos que la ficha calculó y pintó el winrate.
expect(screen.getByText("Winrate: 65%")).toBeInTheDocument();
});
}); Por qué este nivel
- Prueba el caso feliz: la ficha, recién montada, muestra nombre, rol y winrate. Busca el nombre por su ROL de encabezado, no por texto suelto: así el test también verifica que "Genji" es un encabezado de verdad.
- Su límite: solo mira el estado inicial. No toca el botón de favorito —la parte interactiva—, que es justo donde vive la lógica que puede romperse.
// Tier mejor: el caso feliz MÁS la interacción. Probamos que el botón de favorito reacciona.
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
// userEvent simula al usuario de verdad (no un evento sintético suelto). Es asíncrono.
import userEvent from "@testing-library/user-event";
import { TarjetaHeroe, type Heroe } from "./TarjetaHeroe";
const genji: Heroe = { nombre: "Genji", rol: "Daño", partidas: 20, victorias: 13 };
describe("TarjetaHeroe", () => {
it("muestra los datos del héroe", () => {
render(<TarjetaHeroe heroe={genji} />);
// El nombre, por su rol de encabezado; el rol y el winrate, por su texto.
expect(screen.getByRole("heading", { name: "Genji" })).toBeInTheDocument();
expect(screen.getByText("Rol: Daño")).toBeInTheDocument();
expect(screen.getByText("Winrate: 65%")).toBeInTheDocument();
});
it("marca el héroe como favorito al pulsar el botón", async () => {
// setup() prepara el simulador de interacción; se llama una vez por test.
const user = userEvent.setup();
render(<TarjetaHeroe heroe={genji} />);
// Al principio, el botón invita a marcar favorito. Lo buscamos por su rol y su nombre.
const boton = screen.getByRole("button", { name: "Marcar favorito" });
// El usuario hace clic. await: esperamos a que React procese el evento y repinte.
await user.click(boton);
// Tras el clic, el botón cambió a la acción contraria: la ficha REACCIONÓ.
expect(
screen.getByRole("button", { name: "Quitar de favoritos" }),
).toBeInTheDocument();
});
}); Por qué es mejor que el anterior
- Añade la interacción: pulsa el botón con user-event (asíncrono, como una persona) y comprueba que la ficha REACCIONA. Eso es lo que un test de estado inicial no ve.
- Busca el botón por su rol y su nombre accesible. Tras el clic, ese nombre cambia de "Marcar favorito" a "Quitar de favoritos": el cambio de etiqueta es el comportamiento observable que de verdad le importa al usuario.
// Tier excelente: comportamiento completo del toggle (ida y vuelta) y matchers que afirman
// sobre lo que PERCIBE el usuario (texto y aria-pressed), no sobre detalles internos de React.
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { TarjetaHeroe, type Heroe } from "./TarjetaHeroe";
const genji: Heroe = { nombre: "Genji", rol: "Daño", partidas: 20, victorias: 13 };
describe("TarjetaHeroe", () => {
describe("lo que muestra", () => {
it("pinta nombre, rol y winrate calculado", () => {
render(<TarjetaHeroe heroe={genji} />);
// El nombre, por su rol de encabezado (verifica además que es accesible como tal).
expect(screen.getByRole("heading", { name: "Genji" })).toBeInTheDocument();
// El rol, por su texto.
expect(screen.getByText("Rol: Daño")).toBeInTheDocument();
// 13 de 20 partidas = 65%: la ficha calcula el winrate, no lo recibe hecho.
expect(screen.getByText("Winrate: 65%")).toBeInTheDocument();
});
});
describe("el botón de favorito", () => {
it("arranca sin marcar (aria-pressed=false)", () => {
render(<TarjetaHeroe heroe={genji} />);
const boton = screen.getByRole("button", { name: "Marcar favorito" });
// aria-pressed comunica el estado del toggle a un lector de pantalla: al inicio, no.
expect(boton).toHaveAttribute("aria-pressed", "false");
});
it("alterna a favorito y vuelve atrás (ida y vuelta)", async () => {
const user = userEvent.setup();
render(<TarjetaHeroe heroe={genji} />);
// Hay un solo botón: lo cogemos por su rol, sin atarnos a su texto todavía.
const boton = screen.getByRole("button");
// Primer clic: queda marcado. Afirmamos sobre el estado accesible y sobre el texto.
await user.click(boton);
expect(boton).toHaveAttribute("aria-pressed", "true");
expect(boton).toHaveTextContent("Quitar de favoritos");
// Segundo clic: vuelve al estado inicial. El toggle es reversible, no se queda pegado.
await user.click(boton);
expect(boton).toHaveAttribute("aria-pressed", "false");
expect(boton).toHaveTextContent("Marcar favorito");
});
});
}); Por qué es mejor que el anterior
- Prueba el toggle de IDA Y VUELTA: marcar y desmarcar. Un test que solo marca no detecta un botón que se queda pegado y no sabe volver al estado inicial.
- Afirma sobre aria-pressed (toHaveAttribute) y sobre el texto (toHaveTextContent): comprueba el estado COMO LO PERCIBE un lector de pantalla, no un detalle interno de React. Si mañana cambias el useState por otra cosa pero la ficha se comporta igual, estos tests siguen verdes.
- Separa los tests en dos bloques (lo que muestra / el botón) que se leen como la documentación viva del componente.