Uso de jest-dom y Testing Library en entornos React

🧠 Conceptos clave

  • Testing Library es un conjunto de utilidades que facilitan las pruebas de componentes React desde la perspectiva del usuario final, priorizando el comportamiento sobre la implementación interna.
  • jest-dom añade matchers personalizados a Jest (como toBeInTheDocument, toHaveTextContent, toHaveStyle) para verificar el DOM de forma más semántica.
  • Esta combinación (React Testing Library + jest-dom + Jest) constituye el estándar moderno para testeo de interfaces.
  • Se centra en la experiencia del usuario (UX) y no en detalles internos del componente.

⚙️ Instalación y configuración

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest

`

Archivo: jest.setup.ts

import '@testing-library/jest-dom'

Archivo: jest.config.ts

export default {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|scss)$': 'identity-obj-proxy',
  },
}

🧩 Ejemplo básico

// src/components/Saludo.tsx
import React from 'react'

export const Saludo = ({ nombre }: { nombre: string }) => {
  return <h1 data-testid="titulo">Hola, {nombre} 👋</h1>
}
// src/components/Saludo.test.tsx
import { render, screen } from '@testing-library/react'
import { Saludo } from './Saludo'

describe('Saludo', () => {
  test('muestra el nombre correctamente', () => {
    render(<Saludo nombre="Eduardo" />)
    expect(screen.getByText('Hola, Eduardo 👋')).toBeInTheDocument()
  })
})

Resultado:

PASS src/components/Saludo.test.tsx
✓ muestra el nombre correctamente (12 ms)

⚙️ Testing de interacción con userEvent

// src/components/Contador.tsx
import React, { useState } from 'react'

export const Contador = () => {
  const [count, setCount] = useState(0)
  return (
    <>
      <p data-testid="contador">Valor: {count}</p>
      <button onClick={() => setCount(count + 1)}>Incrementar</button>
    </>
  )
}
// src/components/Contador.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Contador } from './Contador'

describe('Contador', () => {
  test('incrementa al hacer clic', async () => {
    const user = userEvent.setup()
    render(<Contador />)

    const boton = screen.getByRole('button', { name: /incrementar/i })
    await user.click(boton)

    expect(screen.getByTestId('contador')).toHaveTextContent('Valor: 1')
  })
})

💡 userEvent simula acciones reales (click, tab, input, teclado, etc.), a diferencia de fireEvent, que es más bajo nivel.


🧠 Ejemplo de pruebas de formularios

// src/components/LoginForm.tsx
import React, { useState } from 'react'

export const LoginForm = ({ onLogin }: { onLogin: (u: string, p: string) => void }) => {
  const [user, setUser] = useState('')
  const [pass, setPass] = useState('')

  return (
    <form
      onSubmit={e => {
        e.preventDefault()
        onLogin(user, pass)
      }}
    >
      <input placeholder="Usuario" value={user} onChange={e => setUser(e.target.value)} />
      <input type="password" placeholder="Contraseña" value={pass} onChange={e => setPass(e.target.value)} />
      <button type="submit">Entrar</button>
    </form>
  )
}
// src/components/LoginForm.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LoginForm } from './LoginForm'

test('envía los datos correctos al login', async () => {
  const mockLogin = vi.fn()
  const user = userEvent.setup()
  render(<LoginForm onLogin={mockLogin} />)

  await user.type(screen.getByPlaceholderText('Usuario'), 'admin')
  await user.type(screen.getByPlaceholderText('Contraseña'), '1234')
  await user.click(screen.getByRole('button', { name: /entrar/i }))

  expect(mockLogin).toHaveBeenCalledWith('admin', '1234')
})

🧩 Testing de componentes con hooks o efectos

// src/components/Loader.tsx
import React, { useEffect, useState } from 'react'

export const Loader = () => {
  const [ready, setReady] = useState(false)

  useEffect(() => {
    const timer = setTimeout(() => setReady(true), 1000)
    return () => clearTimeout(timer)
  }, [])

  return <div>{ready ? 'Listo ✅' : 'Cargando...'}</div>
}
// src/components/Loader.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { Loader } from './Loader'

test('muestra "Listo ✅" después de 1s', async () => {
  render(<Loader />)
  expect(screen.getByText(/cargando/i)).toBeInTheDocument()
  await waitFor(() => expect(screen.getByText(/listo/i)).toBeInTheDocument())
})

🧩 waitFor es útil para pruebas con temporizadores, efectos o peticiones asíncronas.


🧠 Comparativa rápida: fireEvent vs userEvent

Característica fireEvent userEvent
Nivel de abstracción Bajo Alto
Simulación realista ❌ No ✅ Sí
Manejo de focus/blur Manual Automático
Eventos encadenados No
Uso recomendado Casos simples o internos Simular acciones del usuario final

💡 Buenas prácticas

  • Usa screen en lugar de destructuring de render() → mejora legibilidad.
  • Evita probar implementaciones internas, céntrate en la experiencia del usuario.
  • Usa getByRole o getByLabelText en lugar de getByTestId siempre que sea posible.
  • Configura jest.setup.ts para importar @testing-library/jest-dom globalmente.
  • Simula interacciones reales con userEvent en lugar de fireEvent.
  • Usa waitFor o findBy... para elementos renderizados de forma asíncrona.
  • Añade estos tests en pipelines CICD y ejecuta con --coverage en github actions.

📊 Ejemplo de integración en CICD

name: Jest UI Tests
on: [push, pull_request]

jobs:
  test-ui:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run test -- --coverage

📚 Recursos recomendados