Testing de componentes con React Testing Library | Nicolás Garzón
React Testing Library favorece interactuar como lo haría una persona: roles, nombres accesibles y contenido visible.
TypeScript
Copiar render ( < LoginForm onSubmit= { onSubmit} / > ) ;
await user. type ( screen. getByLabelText ( / correo / i ) , "nico@example.com" ) ;
await user. click ( screen. getByRole ( "button" , { name: / ingresar / i } ) ) ;
expect ( onSubmit) . toHaveBeenCalledWith ( { email: "nico@example.com" } ) ;
getByRole.
getByLabelText.
Texto y atributos semánticos.
data-testid como último recurso.
TypeScript
Copiar expect ( await screen. findByText ( / pedido cargado / i ) ) . toBeInTheDocument ( ) ; Usa findBy o waitFor cuando la interfaz cambia después.
Estados y transiciones importantes.
Accesibilidad básica.
Errores, loading y vacío.
Integración entre componentes y usuario.
Evita probar nombres de estado, métodos internos, cantidad exacta de renders o estructura privada.
Simula límites externos, no toda la aplicación. Mock Service Worker permite interceptar red manteniendo un flujo realista.
Disparar eventos de bajo nivel en lugar de userEvent.
Consultar clases CSS.
Hacer snapshots enormes.
Ignorar warnings de act.
Una prueba valiosa protege comportamiento, no implementación incidental.
Texto
Copiar muchas pruebas de funciones y componentes
menos pruebas de integración amplia
pocas E2E críticasNo se trata de una proporción fija. Coloca cada comportamiento en el nivel más pequeño que todavía represente confianza real.
TypeScript
Copiar it ( "submits a valid email" , async ( ) => {
const user = userEvent. setup ( ) ;
const onSubmit = vi. fn ( ) ;
render ( < NewsletterForm onSubmit= { onSubmit} / > ) ;
await user. type (
screen. getByRole ( "textbox" , { name: / correo / i } ) ,
"nico@example.com" ,
) ;
await user. click ( screen. getByRole ( "button" , { name: / suscribirme / i } ) ) ;
expect ( onSubmit) . toHaveBeenCalledWith ( "nico@example.com" ) ;
} ) ; La prueba prepara, interactúa como usuario y verifica el resultado observable.
getByRole obliga a que el elemento tenga rol y nombre accesible correctos. Si falla, revisa primero la interfaz, no cambies inmediatamente a data-testid.
getBy*: debe existir ahora.
queryBy*: puede no existir.
findBy*: aparecerá asíncronamente.
Usa within para limitar a una región:
TypeScript
Copiar const dialog = screen. getByRole ( "dialog" , { name: / eliminar pedido / i } ) ;
expect ( within ( dialog) . getByRole ( "button" , { name: / confirmar / i } ) ) . toBeEnabled ( ) ; userEvent reproduce secuencias más cercanas a interacción: focus, teclado, input y click. fireEvent sigue siendo útil para eventos específicos de bajo nivel, pero puede saltarse comportamiento real.
Siempre crea userEvent.setup() dentro de la prueba para manejar timers y async de forma predecible.
TypeScript
Copiar await user. click ( loadButton) ;
expect ( await screen. findByRole ( "heading" , { name: / pedido 123 / i } ) ) . toBeVisible ( ) ; waitFor es apropiado cuando la expectativa se vuelve verdadera después:
TypeScript
Copiar await waitFor ( ( ) => expect ( mock) . toHaveBeenCalledTimes ( 1 ) ) ; No pongas acciones dentro de waitFor; puede repetirlas.
En vez de mockear el Hook de datos:
TypeScript
Copiar server. use (
http. get ( "/api/orders" , ( ) => HttpResponse. json ( orders) ) ,
) ; La UI ejecuta su flujo normal de fetch, caché y errores. Puedes variar:
Success.
Empty.
Error.
Delay.
Respuesta inválida.
Esto protege el contrato HTTP sin levantar backend real.
Crea una función de render que reciba opciones, no un wrapper global rígido:
TypeScript
Copiar function renderApp ( ui: React. ReactNode, options? : RenderOptions) {
return render (
< TestRouter initialEntries= { options?. routes} >
< SessionProvider value= { options?. session ?? guestSession} >
{ ui}
< / SessionProvider>
< / TestRouter> ,
) ;
} Cada test declara el contexto relevante.
Para probar un componente controlado, crea un harness que actualice state:
TypeScript
Copiar function ControlledTabs ( ) {
const [ value, setValue] = useState ( "details" ) ;
return < Tabs value= { value} onValueChange= { setValue} / > ;
} Pasar un vi.fn() sin rerender puede probar el callback pero no el comportamiento completo.
Labels.
Validación.
Errores asociados.
Pending.
Doble submit.
Conservación de valores al fallar.
Reset tras éxito.
Focus en primer error.
No verifiques state interno; observa campos, mensajes y callbacks.
Un custom hook puede probarse mediante renderHook, pero si su valor solo tiene sentido en UI, un consumidor pequeño ofrece más confianza. Extrae cálculos puros y pruébalos sin React.
Usa fake timers cuando el comportamiento depende del tiempo, pero coordínalos con userEvent:
TypeScript
Copiar vi. useFakeTimers ( ) ;
const user = userEvent. setup ( { advanceTimers: vi. advanceTimersByTime } ) ; Avanza solo el tiempo necesario y restaura timers después. No uses esperas reales largas.
Testing Library consulta document.body, por lo que puede encontrar portales. Verifica:
Dialog por role/name.
Focus inicial.
Escape.
Focus restoration.
Click dentro no cierra accidentalmente.
Fondo no interactuable según implementación.
Controla una Promise diferida para probar fallback y resolución. Para Error Boundary, un componente que lanza permite verificar fallback y retry. No acoples tests a promises internas de una librería.
Puedes envolver tests representativos en StrictMode para descubrir effects sin cleanup. No todos necesitan duplicación. Nunca hagas assertions frágiles sobre número exacto de renders o effects salvo que ese sea el contrato.
Útiles para estructuras pequeñas y estables, pero un snapshot enorme se acepta sin revisar. Prefiere assertions semánticas. Un cambio visual importante necesita visual regression o E2E, no solo HTML serializado.
100% de líneas no garantiza casos útiles. Prioriza:
Reglas de negocio.
Transiciones.
Errores.
Accesibilidad.
Integraciones críticas.
Una línea trivial puede no necesitar test aislado; un caso de carrera sí.
Selectores CSS.
data-testid para controles con rol.
Mocks de todos los hijos.
Assertions de implementación.
Esperas arbitrarias.
Orden global compartido.
State singleton entre tests.
Refactoriza hacia comportamiento y límites reales.
Reserva para flujos que cruzan sistema:
Login.
Compra.
Creación de pedido.
Permisos.
Navegación y refresh.
Integración backend.
Mantén pocos, deterministas y con datos controlados.
Prueba lo que una persona puede ver y hacer.
Queries accesibles mejoran simultáneamente tests y UI.
Mockea límites, no internals.
Asincronía necesita findBy/waitFor, no sleeps.
Distribuye confianza entre unit, integration y E2E.
¿Cuándo usar queryBy?
¿Por qué MSW es mejor que mockear useOrders?
¿Qué problema tiene actuar dentro de waitFor?
¿Qué debería probar un modal?
Ver respuestas
Cuando verificas que algo no existe.
Mantiene el flujo real de la aplicación y simula el límite HTTP.
La callback puede repetirse y duplicar la interacción.
Semántica, focus, teclado, cierre y restauración.
Accesibilidad en componentes React profundiza en los contratos que estas queries ayudan a proteger.