Testing en React 2026: Vitest, React Testing Library y Mejores Prácticas
Dominar el testing en React con Vitest y React Testing Library. Aprender patrones de testing de componentes, manejo asíncrono, estrategias de mocking y mejores prácticas para entrevistas técnicas en 2026.

El testing en React durante 2026 se centra en Vitest como el ejecutor de tests dominante y React Testing Library (RTL) como el estándar para testing de componentes. Esta combinación ofrece ciclos de retroalimentación rápidos, soporte nativo de módulos ES y tests que verifican comportamiento en lugar de detalles de implementación.
Escribir tests que se asemejen a cómo los usuarios interactúan con los componentes. Consultar por roles accesibles, etiquetas y texto—no por clases CSS o atributos data-testid. Este enfoque detecta bugs reales y sobrevive al refactoring.
Configuración de Vitest para Proyectos React
Vitest reemplazó a Jest como el ejecutor de tests preferido para aplicaciones React modernas. El soporte nativo de módulos ES elimina la sobrecarga de transformación, y la integración estrecha con Vite significa que la configuración refleja el entorno de desarrollo.
Una configuración de Vitest lista para producción maneja la transformación JSX, alias de rutas y configuración del entorno de testing sin boilerplate:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
// Use jsdom for DOM APIs and React rendering
environment: 'jsdom',
// Run setup file before each test file
setupFiles: ['./src/test/setup.ts'],
// Include only test files, exclude e2e
include: ['src/**/*.{test,spec}.{ts,tsx}'],
// Enable global test APIs (describe, it, expect)
globals: true,
// Generate coverage reports
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: ['node_modules', 'src/test/**'],
},
},
})El archivo de configuración extiende los matchers y configura la limpieza entre tests:
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'
// Clean up rendered components after each test
afterEach(() => {
cleanup()
})
// Mock window.matchMedia for responsive components
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})Esta configuración proporciona una base limpia para cada test y maneja los mocks comunes de APIs del navegador que jsdom no posee.
Testing de Componentes con React Testing Library
React Testing Library impone probar los componentes desde la perspectiva del usuario. En lugar de inspeccionar el estado interno o valores de props, los tests interactúan con la salida renderizada a través de consultas de accesibilidad.
Consideremos un componente de búsqueda que filtra resultados y muestra estados de carga:
import { useState } from 'react'
interface SearchResultsProps {
onSearch: (query: string) => Promise<string[]>
}
export function SearchResults({ onSearch }: SearchResultsProps) {
const [query, setQuery] = useState('')
const [results, setResults] = useState<string[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
async function handleSearch() {
if (!query.trim()) return
setIsLoading(true)
setError(null)
try {
const data = await onSearch(query)
setResults(data)
} catch (e) {
setError('Search failed. Please try again.')
} finally {
setIsLoading(false)
}
}
return (
<div>
<label htmlFor="search-input">Search</label>
<input
id="search-input"
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button onClick={handleSearch} disabled={isLoading}>
{isLoading ? 'Searching...' : 'Search'}
</button>
{error && <p role="alert">{error}</p>}
<ul aria-label="Search results">
{results.map((result, i) => (
<li key={i}>{result}</li>
))}
</ul>
</div>
)
}Los tests verifican el comportamiento del componente a través de interacciones del usuario y estados asíncronos:
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
import { SearchResults } from './SearchResults'
describe('SearchResults', () => {
it('displays results after successful search', async () => {
// Arrange: mock the search function
const mockSearch = vi.fn().mockResolvedValue(['React', 'Vue', 'Angular'])
const user = userEvent.setup()
render(<SearchResults onSearch={mockSearch} />)
// Act: type a query and click search
await user.type(screen.getByLabelText('Search'), 'framework')
await user.click(screen.getByRole('button', { name: 'Search' }))
// Assert: results appear in the list
expect(await screen.findByText('React')).toBeInTheDocument()
expect(screen.getByText('Vue')).toBeInTheDocument()
expect(mockSearch).toHaveBeenCalledWith('framework')
})
it('shows loading state during search', async () => {
// Create a promise that doesn't resolve immediately
let resolveSearch: (value: string[]) => void
const searchPromise = new Promise<string[]>((resolve) => {
resolveSearch = resolve
})
const mockSearch = vi.fn().mockReturnValue(searchPromise)
const user = userEvent.setup()
render(<SearchResults onSearch={mockSearch} />)
await user.type(screen.getByLabelText('Search'), 'test')
await user.click(screen.getByRole('button', { name: 'Search' }))
// Button shows loading state
expect(screen.getByRole('button', { name: 'Searching...' })).toBeDisabled()
// Resolve the search to clean up
resolveSearch!([])
})
it('displays error message on search failure', async () => {
const mockSearch = vi.fn().mockRejectedValue(new Error('Network error'))
const user = userEvent.setup()
render(<SearchResults onSearch={mockSearch} />)
await user.type(screen.getByLabelText('Search'), 'query')
await user.click(screen.getByRole('button', { name: 'Search' }))
// Error message appears with alert role for screen readers
expect(await screen.findByRole('alert')).toHaveTextContent(
'Search failed. Please try again.'
)
})
})La biblioteca userEvent simula interacciones de usuario realistas incluyendo eventos de teclado y gestión de foco, detectando bugs que los eventos sintéticos omiten.
Patrones de Testing Asíncrono y waitFor
Las operaciones asíncronas requieren estrategias de espera explícitas. React Testing Library proporciona consultas findBy que combinan getBy con espera automática, más waitFor para aserciones complejas.
Preferir findBy sobre waitFor + getBy cuando se espera que aparezcan elementos. Reservar waitFor para aserciones sobre elementos existentes que cambian de estado.
Los patrones asíncronos comunes aparecen al probar hooks y componentes de obtención de datos:
import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, vi } from 'vitest'
// Wait for element to appear (preferred for new elements)
it('loads user profile on mount', async () => {
render(<UserProfile userId="123" />)
// findBy returns a promise, automatically waits up to 1000ms
const heading = await screen.findByRole('heading', { name: /john doe/i })
expect(heading).toBeInTheDocument()
})
// Wait for element to disappear
it('removes loading spinner after data loads', async () => {
render(<DataTable />)
// Wait for spinner to be removed from DOM
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'))
expect(screen.getByRole('table')).toBeInTheDocument()
})
// Wait for multiple conditions
it('enables submit button when form is valid', async () => {
const user = userEvent.setup()
render(<RegistrationForm />)
const submitButton = screen.getByRole('button', { name: 'Submit' })
expect(submitButton).toBeDisabled()
await user.type(screen.getByLabelText('Email'), 'user@example.com')
await user.type(screen.getByLabelText('Password'), 'SecurePass123!')
// waitFor retries the assertion until it passes or times out
await waitFor(() => {
expect(submitButton).toBeEnabled()
})
})
// Avoid false positives with queryBy for absence checks
it('does not show premium badge for free users', async () => {
render(<UserCard subscription="free" />)
// queryBy returns null instead of throwing, use for absence checks
expect(screen.queryByText('Premium')).not.toBeInTheDocument()
})La personalización de timeouts ayuda con operaciones lentas sin ralentizar toda la suite:
// Increase timeout for slow operations
await screen.findByText('Upload complete', {}, { timeout: 5000 })
await waitFor(
() => expect(mockSubmit).toHaveBeenCalled(),
{ timeout: 3000, interval: 100 }
)¿Listo para aprobar tus entrevistas de React / Next.js?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Estrategias de Mocking: Módulos, APIs y Hooks
Las capacidades de mocking de Vitest manejan dependencias externas sin contaminar el grafo de módulos. El mocking estratégico aísla los componentes de llamadas de red, bibliotecas de terceros y dependencias complejas.
El mocking de módulos reemplaza imports a nivel del bundler:
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { fetchUserData } from './api'
// Mock the entire fetch module
vi.mock('./http-client', () => ({
httpClient: {
get: vi.fn(),
},
}))
import { httpClient } from './http-client'
describe('fetchUserData', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('transforms API response into user model', async () => {
// Type-safe mock implementation
vi.mocked(httpClient.get).mockResolvedValue({
data: { id: 1, first_name: 'John', last_name: 'Doe' },
})
const user = await fetchUserData(1)
expect(user).toEqual({
id: 1,
fullName: 'John Doe',
})
})
})Para los hooks de React que dependen de contexto o estado externo, el utilitario renderHook permite testing aislado:
import { renderHook, waitFor } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { useAuth } from './useAuth'
import { AuthProvider } from './AuthContext'
describe('useAuth', () => {
it('returns authenticated user after login', async () => {
const mockLogin = vi.fn().mockResolvedValue({ id: 1, name: 'Alice' })
const { result } = renderHook(() => useAuth(), {
wrapper: ({ children }) => (
<AuthProvider loginFn={mockLogin}>{children}</AuthProvider>
),
})
expect(result.current.user).toBeNull()
expect(result.current.isAuthenticated).toBe(false)
await result.current.login('alice@example.com', 'password')
await waitFor(() => {
expect(result.current.user).toEqual({ id: 1, name: 'Alice' })
expect(result.current.isAuthenticated).toBe(true)
})
})
})MSW (Mock Service Worker) proporciona mocking de APIs a nivel de red, permitiendo tests que ejercitan llamadas fetch reales:
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'Test User',
})
}),
http.post('/api/login', async ({ request }) => {
const body = await request.json()
if (body.email === 'valid@example.com') {
return HttpResponse.json({ token: 'abc123' })
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}),
]Testing de Server Components en React 19
Los Server Components presentan desafíos de testing únicos ya que se ejecutan en el servidor y transmiten HTML al cliente. El testing unitario directo requiere un enfoque diferente al testing de componentes cliente.
Para Server Components que realizan obtención de datos, probar la salida del renderizado con fuentes de datos mockeadas:
import { describe, it, expect, vi } from 'vitest'
// Mock the data fetching function
vi.mock('./db', () => ({
getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Server User' }),
}))
import { render, screen } from '@testing-library/react'
import { UserProfile } from './UserProfile.server'
describe('UserProfile Server Component', () => {
it('renders user data from database', async () => {
// Server components are async, await the component
const Component = await UserProfile({ userId: '1' })
render(Component)
expect(screen.getByRole('heading')).toHaveTextContent('Server User')
})
})Los tests de integración con Playwright proporcionan la cobertura más confiable para Server Components al probar el pipeline de renderizado completo:
import { test, expect } from '@playwright/test'
test('server component renders with streamed data', async ({ page }) => {
await page.goto('/users/1')
// Wait for streaming to complete
await expect(page.getByRole('heading', { name: /profile/i })).toBeVisible()
await expect(page.getByText('Server User')).toBeVisible()
})Testing de Custom Hooks con Estado Complejo
Los hooks personalizados que gestionan estado complejo, efectos secundarios o suscripciones externas se benefician de tests dedicados. El utilitario renderHook de React Testing Library maneja el ciclo de vida y actualizaciones de los hooks.
Consideremos un hook de búsqueda con debounce que se coordina con una API:
import { useState, useEffect, useCallback } from 'react'
export function useDebounceSearch<T>(
searchFn: (query: string) => Promise<T[]>,
delay = 300
) {
const [query, setQuery] = useState('')
const [results, setResults] = useState<T[]>([])
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
if (!query) {
setResults([])
return
}
setIsLoading(true)
const timeoutId = setTimeout(async () => {
try {
const data = await searchFn(query)
setResults(data)
} finally {
setIsLoading(false)
}
}, delay)
return () => clearTimeout(timeoutId)
}, [query, searchFn, delay])
return { query, setQuery, results, isLoading }
}Los tests verifican el comportamiento del debounce y las transiciones de estado:
import { renderHook, act, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useDebounceSearch } from './useDebounceSearch'
describe('useDebounceSearch', () => {
beforeEach(() => {
vi.useFakeTimers()
})
it('debounces search calls', async () => {
const mockSearch = vi.fn().mockResolvedValue(['result1', 'result2'])
const { result } = renderHook(() => useDebounceSearch(mockSearch, 300))
// Type multiple characters quickly
act(() => result.current.setQuery('r'))
act(() => result.current.setQuery('re'))
act(() => result.current.setQuery('rea'))
act(() => result.current.setQuery('reac'))
act(() => result.current.setQuery('react'))
// Search not called yet (debouncing)
expect(mockSearch).not.toHaveBeenCalled()
// Advance timers past debounce delay
await act(async () => {
vi.advanceTimersByTime(300)
})
// Only one search call with final query
expect(mockSearch).toHaveBeenCalledTimes(1)
expect(mockSearch).toHaveBeenCalledWith('react')
})
it('shows loading state during search', async () => {
let resolveSearch: (value: string[]) => void
const mockSearch = vi.fn().mockImplementation(
() => new Promise((resolve) => { resolveSearch = resolve })
)
const { result } = renderHook(() => useDebounceSearch(mockSearch, 100))
act(() => result.current.setQuery('test'))
await act(async () => {
vi.advanceTimersByTime(100)
})
expect(result.current.isLoading).toBe(true)
await act(async () => {
resolveSearch(['result'])
})
expect(result.current.isLoading).toBe(false)
expect(result.current.results).toEqual(['result'])
})
})Los fake timers de Vitest permiten testing determinístico del comportamiento dependiente del tiempo sin demoras reales.
Preguntas de Entrevista sobre Testing en React
Las entrevistas técnicas frecuentemente indagan sobre conocimientos de testing. Estas preguntas evalúan la comprensión práctica en lugar de conceptos teóricos.
Evitar probar detalles de implementación como valores de estado o métodos de componentes. Los tests que fallan al refactorizar la estructura interna proporcionan falsa confianza y carga de mantenimiento.
Prioridad de consultas en React Testing Library
La prioridad de consulta recomendada sigue la accesibilidad: getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId. Las consultas basadas en roles aseguran que los componentes permanezcan accesibles y los tests sobrevivan a cambios de markup.
Cuándo usar findBy vs waitFor
Las consultas findBy esperan a que los elementos aparezcan en el DOM y devuelven una promesa. Usarlas cuando se esperan nuevos elementos después de operaciones asíncronas. waitFor envuelve aserciones que pueden no pasar inmediatamente, reintentando hasta el éxito o timeout. Usar waitFor para aserciones sobre cambios de estado en elementos existentes.
Mocking de fetch vs MSW
El mocking directo de fetch (vi.spyOn(global, 'fetch')) funciona para casos simples pero requiere reimplementar objetos Response y manejo de errores. MSW intercepta a nivel de red, permitiendo a los tests ejercitar llamadas fetch reales, serialización de solicitudes y rutas de código de manejo de errores.
Para práctica más profunda con estos conceptos, explorar las preguntas de entrevista de React Testing en SharpSkill.
Conclusión
- Configurar Vitest con entorno jsdom, archivos de configuración para extensiones de matchers y reportes de cobertura para visibilidad sobre brechas de cobertura de tests
- Consultar componentes por roles y etiquetas accesibles para escribir tests que verifican comportamiento orientado al usuario y detectan bugs reales
- Usar consultas
findBypara elementos que aparecen después de operaciones asíncronas,waitForpara aserciones sobre estado cambiante - Mockear al nivel apropiado: mocks de módulos para aislamiento unitario, MSW para testing realista de la capa de red
- Probar hooks personalizados con
renderHooky fake timers para verificar gestión de estado complejo y efectos secundarios - Los Server Components requieren renderizado asíncrono en tests o tests de integración completos con Playwright
- Lectura relacionada: Patrones avanzados de React Hooks y TypeScript con React para patrones de testing type-safe
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Etiquetas
Compartir
Artículos relacionados

React Compiler en 2026: memoización automática y preguntas de entrevista
Análisis completo del React Compiler en 2026: pipeline de compilación, memoización automática, reglas de React y preguntas frecuentes en entrevistas técnicas.

Top 30 Preguntas de Entrevista React: Guía Completa para Triunfar
Las 30 preguntas de entrevista React más frecuentes en 2026. Respuestas detalladas, ejemplos de código y consejos para conseguir el puesto de desarrollador React.

Server Actions de Next.js 16 en 2026: mutaciones, revalidación y preguntas de entrevista
Cómo las Server Actions de Next.js 16 manejan mutaciones, revalidación, estado pendiente, UI optimista y seguridad, con las preguntas de entrevista que evalúan cada concepto.