getByRole, findByRole y waitFor: cuándo usar cada uno
Si alguna vez has arreglado un test metiendo un waitFor “por si acaso”, este post es para ti.
Cuando un test de frontend falla de forma intermitente, muchas veces el problema no está en el componente, sino en cómo estamos buscando los elementos. Testing Library te da varias formas de mirar el DOM, pero cada una cuenta una historia distinta.
No es lo mismo decir “esto ya debería estar” que decir “esto aparecerá después” o “esto no debería existir”. Ahí está la diferencia entre screen.getByRole, screen.findByRole, screen.queryByRole y waitFor.
La regla rápida es esta:
| Query | Espera | Si no encuentra | Úsala cuando |
|---|---|---|---|
getBy... | No | Lanza error | El elemento debe estar ya en pantalla |
findBy... | Sí | Lanza error tras timeout | El elemento aparecerá después de una acción asíncrona |
queryBy... | No | Devuelve null | Quieres comprobar que algo no está |
waitFor | Sí | Reintenta una aserción | Esperas un efecto que no es solo encontrar un elemento |
Empieza por el rol
La query más recomendable suele ser getByRole o alguna de sus variantes (findByRole, queryByRole, getAllByRole, etc.).
El motivo es sencillo: se acerca a cómo una persona o una tecnología asistiva encuentra la interfaz. Botones, enlaces, encabezados, campos de texto, diálogos, alertas. Si tu test puede encontrar algo por rol, normalmente tu interfaz también está contando mejor lo que es.
screen.getByRole("button", { name: /guardar/i });
screen.getByRole("heading", { name: /perfil/i });
screen.getByRole("link", { name: /volver/i });
El segundo argumento, name, es clave. Evita buscar “un botón cualquiera” y deja escrita la intención del test.
// Peor: puede haber varios botones.
screen.getByRole("button");
// Mejor: describe el botón que una persona intentaría pulsar.
screen.getByRole("button", { name: /crear cuenta/i });
getByRole: para lo que ya existe
screen.getByRole es síncrono. Busca una vez y devuelve el elemento si lo encuentra.
Si no lo encuentra, falla inmediatamente. Si encuentra más de una coincidencia, también falla.
Es una forma de decirle al test: “esto forma parte del estado actual de la pantalla”.
render(<LoginForm />);
expect(
screen.getByRole("heading", { name: /iniciar sesión/i }),
).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /entrar/i }));
Úsalo cuando el elemento debería estar disponible justo después del render o después de una acción que ya ha terminado.
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole("button", { name: /incrementar/i }));
expect(screen.getByRole("status")).toHaveTextContent("1");
En este ejemplo no hace falta findByRole ni waitFor si el click actualiza el estado de forma inmediata. Añadir espera aquí solo mete ruido y hace que el test parezca más complejo de lo que es.
findByRole: para lo que aparecerá después
screen.findByRole es asíncrono. Devuelve una promesa y reintenta la búsqueda hasta que el elemento aparece o se agota el timeout.
Es la opción correcta cuando el DOM cambia después de una promesa, una petición simulada, un indicador de carga o cualquier actualización asíncrona.
Con findByRole estás diciendo: “esto todavía no está, pero debe aparecer”.
const user = userEvent.setup();
render(<UserSearch />);
await user.type(screen.getByRole("textbox", { name: /usuario/i }), "ada");
await user.click(screen.getByRole("button", { name: /buscar/i }));
expect(
await screen.findByRole("heading", { name: /ada lovelace/i }),
).toBeInTheDocument();
Internamente, puedes pensar en findByRole como una combinación de getByRole y waitFor.
Por eso esto suele ser innecesario:
// Evítalo: findByRole ya espera.
await waitFor(async () => {
expect(await screen.findByRole("alert")).toHaveTextContent(/guardado/i);
});
Mejor:
expect(await screen.findByRole("alert")).toHaveTextContent(/guardado/i);
queryByRole: para comprobar ausencia
screen.queryByRole es síncrono, pero no lanza error cuando no encuentra nada. Devuelve null.
Eso lo hace perfecto para afirmar que algo no está en el DOM. Es la query de las ausencias.
render(<Dashboard />);
expect(screen.queryByRole("alert", { name: /error/i })).not.toBeInTheDocument();
No uses getByRole para comprobar ausencia, porque el propio getByRole fallaría antes de llegar al expect.
// Mal: getByRole lanza error si no encuentra el elemento.
expect(screen.getByRole("alert")).not.toBeInTheDocument();
// Bien.
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
getAllBy, findAllBy y queryAllBy
Cuando esperas más de un elemento, usa las variantes All. No es un detalle menor: si tu pantalla tiene tres elementos y el test busca uno solo, estás dejando ambigüedad dentro del test.
expect(screen.getAllByRole("listitem")).toHaveLength(3);
Las reglas son las mismas:
| Query | Si no encuentra | Si encuentra uno o más |
|---|---|---|
getAllBy... | Lanza error | Devuelve array |
findAllBy... | Lanza error tras timeout | Devuelve promesa con array |
queryAllBy... | Devuelve [] | Devuelve array |
Para ausencia múltiple, queryAllBy... suele ser más claro.
expect(screen.queryAllByRole("listitem")).toHaveLength(0);
Cuándo usar waitFor
waitFor sirve para esperar a que una aserción deje de fallar.
No espera porque devuelvas false. Reintenta porque dentro se lanza un error, normalmente desde un expect. Esta parte es importante: waitFor no espera “un rato”; espera a que una expectativa se cumpla.
await waitFor(() => {
expect(api.saveUser).toHaveBeenCalledTimes(1);
});
Es útil cuando lo que quieres esperar no es simplemente que aparezca un elemento. Para encontrar elementos que aparecen, normalmente findBy... expresa mejor la intención.
Casos habituales:
- Esperar a que un mock haya sido llamado.
- Esperar a que una función se llame con ciertos argumentos.
- Esperar a que una URL, un store o un estado externo cambie.
- Esperar a que un atributo o texto cambie cuando no tienes una query más directa.
await user.click(screen.getByRole("button", { name: /guardar/i }));
await waitFor(() => {
expect(saveUser).toHaveBeenCalledWith({ name: "Ada" });
});
Cuándo no usar waitFor
No uses waitFor si una query asíncrona expresa mejor la intención. waitFor no debería ser el comodín que tapa cualquier duda del test.
// Más ruido del necesario.
await waitFor(() => {
expect(screen.getByRole("alert")).toHaveTextContent(/guardado/i);
});
Si la alerta aparece después de la acción, usa findByRole:
expect(await screen.findByRole("alert")).toHaveTextContent(/guardado/i);
Tampoco metas acciones de usuario dentro de waitFor. El callback puede ejecutarse varias veces, así que también podrías estar haciendo click varias veces sin darte cuenta.
// Mal: la acción puede ejecutarse varias veces.
await waitFor(async () => {
await user.click(screen.getByRole("button", { name: /guardar/i }));
expect(await screen.findByRole("alert")).toBeInTheDocument();
});
Mejor: ejecuta la acción una vez y espera el resultado.
await user.click(screen.getByRole("button", { name: /guardar/i }));
expect(await screen.findByRole("alert")).toBeInTheDocument();
Esperar a que algo desaparezca
Para desapariciones, la alternativa más clara suele ser waitForElementToBeRemoved.
await waitForElementToBeRemoved(() => screen.queryByRole("status"));
También puedes usar waitFor con queryByRole.
await waitFor(() => {
expect(screen.queryByRole("status")).not.toBeInTheDocument();
});
La diferencia importante es que para desaparición debes usar queryBy..., no getBy..., porque quieres permitir que el elemento no exista. Si usas getBy..., el test falla justo en el momento en el que la pantalla se comporta como esperabas.
Otras formas de buscar en un test
Testing Library recomienda buscar de la forma más parecida posible a como una persona usa la interfaz. Esa es la brújula: primero lo accesible y semántico; al final, los detalles internos.
El orden práctico sería:
getByRoleconnamepara botones, enlaces, encabezados, diálogos, alertas y la mayoría de elementos interactivos.getByLabelTextpara campos de formulario, especialmente cuando el rol no es suficiente.getByPlaceholderTextsolo si no hay label, aunque el placeholder no debería sustituir al label.getByTextpara textos no interactivos.getByDisplayValuepara valores actuales de inputs.getByAltTextpara imágenes relevantes.getByTitlesi eltitleforma parte real de la experiencia.getByTestIdcomo último recurso.
Un caso típico: input type="password" no tiene rol implícito, así que aquí getByLabelText es mejor que intentar forzar getByRole.
screen.getByLabelText(/contraseña/i);
Limita la búsqueda con within
Si hay elementos repetidos, en vez de usar textos demasiado específicos, puedes acotar la búsqueda a una zona.
const row = screen.getByRole("row", { name: /ada lovelace/i });
await user.click(within(row).getByRole("button", { name: /editar/i }));
Esto hace que el test diga algo muy humano: “dentro de la fila de Ada, pulsa editar”. Menos magia, más contexto.
Checklist rápido
Si dudas, decide así:
| Situación | Usa |
|---|---|
| El elemento debe existir ya | getByRole |
| El elemento aparecerá después | findByRole |
| El elemento no debe existir | queryByRole |
| Hay varios elementos | getAllByRole, findAllByRole o queryAllByRole |
| Esperas una llamada a un mock | waitFor |
| Esperas que desaparezca un loader | waitForElementToBeRemoved |
| Buscas dentro de una sección concreta | within |
La idea no es memorizar todas las APIs. La idea es que la query cuente la intención del test: existe ahora, aparecerá después, no debe estar, hay varios o estoy esperando un efecto secundario.
Un buen test no solo comprueba que la aplicación funciona. También deja una pista clara de cómo debería comportarse la pantalla. Elegir bien entre getByRole, findByRole, queryByRole y waitFor es una forma pequeña, pero muy efectiva, de escribir tests que se leen mejor y fallan por mejores motivos.