Mejores prácticas de testing
Un resumen condensado de las 25 mejores prácticas más importantes extraídas de todas las páginas de esta sección.
- Prefiere getByRole sobre test IDs: Recorre la prioridad de consultas de Testing Library (
getByRole→getByLabelText→getByText→getByTestId) para que las aserciones coincidan con el árbol de accesibilidad; usargetByTestIdprimero acopla los tests a la implementación y pasa por alto regresiones de a11y. - queryBy para ausencia, getBy para presencia:
getBy*lanza una excepción cuando no hay coincidencias yfindBy*reintenta, así que soloqueryBy*es seguro para aserciones de «no debería existir» — usargetBy*para ausencia produce un error confuso de «no encontrado» en lugar de una aserción fallida clara. - Delimita las consultas con within: Cuando los tests renderizan elementos de lista que comparten etiquetas o botones, envuelve las consultas en
within(listItem)(o unaria-labelcon nombre único) para apuntar exactamente a la fila que quieres en lugar de la primera coincidencia de la página. - Espera (await) cada llamada a userEvent: Cada método de
userEvent(v14+) devuelve una Promise, así que olvidarawaithace que la siguiente aserción se ejecute antes de que el evento se procese y convierte tests fallidos en falsos positivos silenciosos — siempreawait userEvent.click(button). - Prefiere userEvent sobre fireEvent:
userEvent.typedespacha la secuencia completa focus → keydown → keypress → input → keyup, que es lo que escuchan los componentes y validadores reales;fireEvent.changesolo establece el valor y omite los eventos de pulsación de tecla de los que dependen bibliotecas como React Hook Form. - Envuelve Jest con createJestConfig: Exporta tu configuración de Jest a través de
createJestConfigdenext/jestpara obtener gratis las transformaciones SWC, los mocks de módulos CSS y la resolución de env/alias consciente denext.config.js; pon los matchers de jest-dom ensetupFilesAfterSetup(no ensetupFiles) o las extensiones deexpectno estarán en el ámbito. - Configura Vitest explícitamente: Vitest necesita
@vitejs/plugin-reactpara JSX,environment: "jsdom",globals: true, una importación de@testing-library/jest-dom/vitesten el archivo de setup y"vitest/globals"entsconfig.compilerOptions.types— si falta cualquiera, JSX o los tipos de matchers fallan en silencio. - Usa vi.hoisted para referencias de factory de mocks:
vi.mock()se eleva (hoist) por encima de los imports, así que las variables declaradas en el cuerpo del módulo sonundefineddentro de la factory; usavi.hoisted(() => ({ mockPush: vi.fn() }))y referencia esa ref compartida tanto desde la factory como desde tus tests. - Limpia los mocks entre tests: Los valores de retorno y el historial de llamadas de los mocks se filtran entre tests del mismo archivo, así que llama a
vi.clearAllMocks()(ojest.clearAllMocks()) enbeforeEach; lo mismo aplica avi.restoreAllMocks()para spies yserver.resetHandlers()para MSW. - findBy para aparición, waitFor para estado: Usa
findByRole/findByTextcuando esperas que aparezca un elemento, ywaitForsolo cuando la aserción no es sobre un elemento (p. ej., un contador de llamadas de efecto secundario) —findByencapsula el reintento y ofrece mejores mensajes de error. - Sin efectos secundarios en waitFor:
waitForsondea su callback cada ~50 ms, así que poneruserEvent.click()o un spy de fetch dentro dispara el evento repetidamente y corrompe el state; haz la interacción fuera dewaitFory deja solo las aserciones dentro. - Reinicia MSW entre tests: Llama a
server.resetHandlers()enafterEachpara que las sobrescrituras por test no se filtren al siguiente, y manténonUnhandledRequest: "error"para que cualquier endpoint olvidado salga a la superficie de inmediato en lugar de enmascararse como un cuelgue misterioso. - Reinicia el state de Zustand por test: Los stores de Zustand son singletons de módulo, así que el state persiste entre tests del mismo archivo a menos que lo reinicies con
useCartStore.setState({ items: [] })(ogetState().reset()) enbeforeEach— de lo contrario, las mutaciones de un test presembran invisiblemente el siguiente. - QueryClient nuevo por test: Crea un
QueryClientnuevo conretry: falsedentro de un helperrenderWithProviderspara cada test; un cliente compartido cachea respuestas entre tests y cuelga las suites en reintentos fallidos. - Consulta role=alert para errores de RHF: Renderiza los errores de validación con
role="alert"para que se anuncien al lector de pantalla y sean consultables comoawait screen.findAllByRole("alert"); recuerda que React Hook Form valida al enviar por defecto, así que aserciones de errores antes del envío no encuentran nada a menos que configuresmode: "onChange". - Mockea useActionState en jsdom:
useActionStateno puede ejecutar realmente una Server Action bajo jsdom, así que mockea el hook para devolver una tupla controlada[state, formAction, isPending]al probar la UI de error/pendiente del formulario — probar la acción real pertenece a un test de integración en Node/Vitest. - Envuelve las actualizaciones de state del hook en act: En
renderHook, cualquier llamada síncrona que provoque una actualización de state necesitaact(() => result.current.increment())o obtienes una advertencia más unresult.currentobsoleto; con temporizadores falsos, avanza también dentro deacto pasa{ shouldAdvanceTime: true }. - No desestructures result.current:
result.currentes una referencia viva que cambia después de cada renderizado, así queconst { count } = result.currentcaptura una instantánea que queda obsoleta — lee siempreresult.current.countdirectamente en cada aserción. - Espera primero los Server Components asíncronos:
render()no acepta una Promise, así que prueba Server Components asíncronos llamándolos como funciones y esperando el JSX:const ui = await PostList({ id: "1" }); render(ui);— pasar el componente directamente lanza una excepción. - Mockea next/headers y next/cache:
cookies(),headers(),revalidatePathyrevalidateTaglanzan excepciones fuera del contexto de petición de Next.js, así que sustitúyelos convi.mock("next/headers", …)yvi.mock("next/cache", …)— también mockeanotFound()para que lance un error centinela yawait expect(fn()).rejects.toThrow()funcione. - Usa locators accesibles de Playwright + POM: Prefiere
page.getByRole,getByLabelygetByTextsobre selectores CSS y encapsúlalos en clases Page Object; selectores CSS como.btn-primary.mt-4se rompen en cuanto alguien renombra una clase. - Reemplaza waitForTimeout con auto-espera: Las aserciones
expect()de Playwright reintentan automáticamente hasta 5 segundos, así queawait expect(page).toHaveURL("/dashboard")reemplaza cadapage.waitForTimeout(500)— las pausas fijas son la causa n.º 1 de tests E2E inestables. - Arranque automático del servidor + trazas en reintento: Configura
webServerenplaywright.config.tspara arrancar Next.js automáticamente conreuseExistingServer: !CI, y habilitatrace: "on-first-retry"para que las ejecuciones fallidas incluyan una línea de tiempo completa de DOM/red/consola que puedas abrir conplaywright show-trace. - Autentica una vez, registra rutas antes de goto: Usa un proyecto
setupmásstorageState: "e2e/.auth/user.json"para que cada test empiece autenticado sin pagar el coste del login, y registra mocks depage.route()antes depage.goto()— las rutas configuradas después de la navegación pierden la carga inicial de la página. - Sigue el Testing Trophy y escribe regresiones: Prioriza comprobaciones estáticas (TypeScript strict, ESLint) y tests de integración primero, usa E2E con moderación para flujos críticos (login, checkout), fija la cobertura cerca del 80 % en lugar de perseguir el 100 %, y escribe un test de regresión por cada bug que corrijas.