Cambiar tema

Pruebas unitarias en Next.js: guía completa de configuración con Jest + React Testing Library

Easton editorial illustration: component assembly loom

El lunes a las diez de la mañana, el líder técnico escribió en el grupo: «Esta semana empezamos con pruebas unitarias; en Next.js usamos Jest.»

Ya había probado React, pero con App Router y Server Components no tenía ni idea. Abrí el proyecto, ejecuté npm install jest y lancé npm test con confianza. La pantalla se llenó de errores rojos:

Error: Cannot use import statement outside a module
SyntaxError: Unexpected token 'export'
Cannot find module 'next/navigation'

Dos días enteros saltando entre archivos de configuración, Stack Overflow y GitHub Issues. Probé más de diez configuraciones distintas; cuando las pruebas por fin pasaron, casi tiré el teclado al aire.

Configurar el entorno de pruebas en Next.js es más complejo de lo que parece, pero tampoco es magia negra: basta con entender unas pocas configuraciones clave y evitar las trampas habituales. Esta guía te lleva desde cero con Next.js 15 + Jest + React Testing Library, y comparte los errores que cometí al probar Client Components, Server Components, hooks y mocks de API.

Configuración del entorno de pruebas (de cero a uno)

Primero lo primero: haz que el entorno funcione. Esta parte puede parecer aburrida, pero explicaré por qué necesitas cada opción para que no copies código sin entender nada.

Instalar dependencias

Abre la terminal e instala todos estos paquetes de una vez:

npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node @types/jest

Resumen rápido de cada paquete:

  • jest: el framework de pruebas
  • jest-environment-jsdom: simula el entorno del navegador (los componentes React necesitan DOM)
  • @testing-library/react: utilidades para probar componentes React
  • @testing-library/jest-dom: aserciones adicionales (como toBeInTheDocument())
  • ts-node y @types/jest: soporte para TypeScript (si usas JS puedes omitirlos)

Tras instalar, no ejecutes pruebas todavía: aún faltan los archivos de configuración.

Crear jest.config.ts

Este es el archivo de configuración más importante. Crea jest.config.ts en la raíz del proyecto (.js si usas JavaScript):

import type { Config } from 'jest'
import nextJest from 'next/jest'

// Esta función carga automáticamente la configuración de Next.js
const createJestConfig = nextJest({
  dir: './', // raíz del proyecto Next.js
})

const config: Config = {
  coverageProvider: 'v8', // herramienta de cobertura de código
  testEnvironment: 'jsdom', // simula el entorno del navegador
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], // configuración previa a las pruebas
}

// Envuelve la configuración con createJestConfig para manejar las transformaciones de Next.js
export default createJestConfig(config)

Punto clave: ¿por qué envolver la configuración con next/jest?

next/jest hace automáticamente lo siguiente:

  • Procesa archivos .css y .module.css (los mockea; si no, las pruebas fallan)
  • Maneja imágenes, fuentes y otros recursos estáticos
  • Carga variables de entorno de .env
  • Transforma TypeScript y JSX
  • Excluye los directorios node_modules y .next

Sin esto tendrías que configurarlo todo a mano. Créeme, es un suplicio.

Crear jest.setup.ts

Crea otro archivo jest.setup.ts en la raíz. El contenido es muy simple:

import '@testing-library/jest-dom'

Esta línea importa los matchers personalizados de Jest DOM y te permite usar aserciones como:

  • expect(element).toBeInTheDocument()
  • expect(element).toHaveClass('active')
  • expect(element).toBeVisible()

Sin este archivo, ninguno de estos métodos funcionará.

Configurar alias de rutas (si usas rutas @/)

Si tu proyecto usa alias como import Button from '@/components/Button', debes indicarle a Jest cómo resolver esas rutas.

Comprueba si tu tsconfig.json (o jsconfig.json) tiene esta configuración:

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/components/*": ["components/*"],
      "@/lib/*": ["lib/*"]
    }
  }
}

Luego añade moduleNameMapper en jest.config.ts:

const config: Config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  // añade esta sección
  moduleNameMapper: {
    '^@/components/(.*)$': '<rootDir>/components/$1',
    '^@/lib/(.*)$': '<rootDir>/lib/$1',
  },
}

¿Por qué? Jest no reconoce rutas @/ por defecto; solo entiende rutas relativas o absolutas. Debes decirle: «cuando veas @/components/Button, busca el archivo en <rootDir>/components/Button».

Añadir scripts de prueba

Por último, añade dos scripts en package.json:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch"
  }
}
  • npm test: ejecuta todas las pruebas una vez
  • npm run test:watch: modo watch; vuelve a ejecutar al cambiar archivos

Verificar la configuración

Ejecuta una prueba sencilla para verificar la configuración. Crea __tests__/example.test.ts:

describe('Example Test', () => {
  it('should pass', () => {
    expect(1 + 1).toBe(2)
  })
})

Ejecuta npm test. Si ves PASS en verde y 1 passed, enhorabuena: la configuración funciona.

Si falla, no entres en pánico: revisa el capítulo de resolución de problemas; el 90% de los errores tienen solución allí.

Pruebas de componentes en la práctica

Con la configuración lista, toca escribir pruebas de verdad. Probar componentes es el núcleo en Next.js, pero Client Components y Server Components se prueban de forma muy distinta.

Probar Client Components (flujo estándar)

Los Client Components son los que llevan 'use client'; probarlos es directo.

Supón que tienes un formulario de inicio de sesión LoginForm.tsx:

'use client'

import { useState } from 'react'

export default function LoginForm() {
  const [email, setEmail] = useState('')
  const [error, setError] = useState('')

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (!email.includes('@')) {
      setError('请输入有效的邮箱')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="邮箱"
      />
      {error && <span role="alert">{error}</span>}
      <button type="submit">登录</button>
    </form>
  )
}

Archivo de prueba LoginForm.test.tsx:

import { render, screen, fireEvent } from '@testing-library/react'
import LoginForm from '@/components/LoginForm'

describe('LoginForm', () => {
  it('should render input and button', () => {
    render(<LoginForm />)

    // 检查输入框是否存在
    const emailInput = screen.getByPlaceholderText('邮箱')
    expect(emailInput).toBeInTheDocument()

    // 检查按钮是否存在
    const submitButton = screen.getByRole('button', { name: '登录' })
    expect(submitButton).toBeInTheDocument()
  })

  it('should show error for invalid email', () => {
    render(<LoginForm />)

    const emailInput = screen.getByPlaceholderText('邮箱')
    const submitButton = screen.getByRole('button', { name: '登录' })

    // 模拟用户输入
    fireEvent.change(emailInput, { target: { value: 'invalid-email' } })
    fireEvent.click(submitButton)

    // 检查错误提示
    const errorMessage = screen.getByRole('alert')
    expect(errorMessage).toHaveTextContent('请输入有效的邮箱')
  })
})

Enfoque de prueba:

  1. Usa render() para renderizar el componente
  2. Usa screen.getByXxx() para encontrar elementos (por rol, texto, placeholder, etc.)
  3. Usa fireEvent para simular interacciones del usuario
  4. Usa expect() para las aserciones

Consejo: prefiere getByRole frente a getByTestId. Los roles (button, alert, etc.) se acercan más a lo que ve el usuario y hacen las pruebas más estables.

Probar Server Components (algo incómodo)

Los Server Components son clave en Next.js 15, pero Jest no los soporta bien.

Problema principal: Jest no admite Server Components async.

Por ejemplo, un componente que obtiene datos de una API:

// app/posts/page.tsx (Server Component)
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Si pruebas ese componente directamente, Jest lanzará: Objects are not valid as a React child.

¿Qué hacer entonces?

Tres enfoques:

Enfoque 1: extraer la lógica de negocio y probar funciones puras

// lib/posts.ts
export async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  if (!res.ok) throw new Error('Failed to fetch')
  return res.json()
}

// lib/posts.test.ts
import { getPosts } from './posts'

describe('getPosts', () => {
  it('should fetch posts successfully', async () => {
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve([{ id: 1, title: 'Test Post' }]),
      })
    ) as jest.Mock

    const posts = await getPosts()
    expect(posts).toHaveLength(1)
    expect(posts[0].title).toBe('Test Post')
  })
})

Así pruebas la lógica de obtención de datos, no el componente. Extrae la lógica async compleja y deja el componente con renderizado simple.

Enfoque 2: probar Server Components síncronos

Si el Server Component no es async, puedes probarlo con normalidad:

// components/Title.tsx (Server Component, no async)
export default function Title({ text }: { text: string }) {
  return <h1 className="title">{text}</h1>
}

// components/Title.test.tsx
import { render, screen } from '@testing-library/react'
import Title from './Title'

describe('Title', () => {
  it('should render title', () => {
    render(<Title text="Hello World" />)
    const heading = screen.getByRole('heading', { level: 1 })
    expect(heading).toHaveTextContent('Hello World')
  })
})

Enfoque 3: complementar con pruebas E2E

Para Server Components complejos, Playwright o Cypress son más fiables. Jest cubre la lógica del frontend; E2E cubre el flujo completo.

Mi enfoque: extraer la lógica principal en funciones puras, dejar renderizado simple en Server Components y cubrir el resto con E2E.

Consejos para pruebas de interacción

Al probar interacciones, @testing-library/react ofrece muchos métodos:

import { render, screen, fireEvent, waitFor } from '@testing-library/react'

// Clic
fireEvent.click(button)

// Entrada de texto
fireEvent.change(input, { target: { value: 'test' } })

// Esperar actualizaciones async
await waitFor(() => {
  expect(screen.getByText('Success')).toBeInTheDocument()
})

// Comprobar visibilidad
expect(element).toBeVisible()

// Comprobar clase CSS
expect(element).toHaveClass('active')

Ejemplo completo de prueba async de interacción:

it('should submit form successfully', async () => {
  // Mock API
  global.fetch = jest.fn(() =>
    Promise.resolve({
      ok: true,
      json: () => Promise.resolve({ success: true }),
    })
  ) as jest.Mock

  render(<LoginForm />)

  const emailInput = screen.getByPlaceholderText('邮箱')
  const submitButton = screen.getByRole('button', { name: '登录' })

  fireEvent.change(emailInput, { target: { value: '[email protected]' } })
  fireEvent.click(submitButton)

  // 等待成功提示出现
  await waitFor(() => {
    expect(screen.getByText('登录成功')).toBeInTheDocument()
  })
})

Fíjate en waitFor: espera a que terminen las operaciones async antes de comprobar. Si hay useEffect o actualizaciones async, es obligatorio; si no, la prueba fallará antes de que cambie el estado.

Estrategias para probar hooks

Los hooks personalizados son la esencia de React, pero ¿cómo probarlos? Algunos los prueban dentro de componentes y mezclan lógicas. Mejor usar renderHook.

Probar hooks simples

Supón un hook contador:

// hooks/useCounter.ts
import { useState } from 'react'

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)

  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  const reset = () => setCount(initialValue)

  return { count, increment, decrement, reset }
}

Prueba:

// hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('should initialize with default value', () => {
    const { result } = renderHook(() => useCounter())
    expect(result.current.count).toBe(0)
  })

  it('should initialize with custom value', () => {
    const { result } = renderHook(() => useCounter(10))
    expect(result.current.count).toBe(10)
  })

  it('should increment count', () => {
    const { result } = renderHook(() => useCounter())

    act(() => {
      result.current.increment()
    })

    expect(result.current.count).toBe(1)
  })

  it('should reset count', () => {
    const { result } = renderHook(() => useCounter(5))

    act(() => {
      result.current.increment()
      result.current.increment()
    })

    expect(result.current.count).toBe(7)

    act(() => {
      result.current.reset()
    })

    expect(result.current.count).toBe(5)
  })
})

Puntos clave:

  • renderHook renderiza el hook
  • act envuelve actualizaciones de estado (regla de React)
  • result.current obtiene el valor devuelto por el hook

Probar hooks que dependen de Context

Si el hook depende de Context (p. ej. Auth Context), necesitas un Provider.

// hooks/useAuth.ts
import { useContext } from 'react'
import { AuthContext } from '@/contexts/AuthContext'

export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

Prueba con un Provider mock:

// hooks/useAuth.test.tsx
import { renderHook } from '@testing-library/react'
import { useAuth } from './useAuth'
import { AuthContext } from '@/contexts/AuthContext'

describe('useAuth', () => {
  it('should return auth context value', () => {
    const mockAuthValue = {
      user: { id: 1, name: 'Test User' },
      login: jest.fn(),
      logout: jest.fn(),
    }

    const wrapper = ({ children }: { children: React.ReactNode }) => (
      <AuthContext.Provider value={mockAuthValue}>
        {children}
      </AuthContext.Provider>
    )

    const { result } = renderHook(() => useAuth(), { wrapper })

    expect(result.current.user).toEqual({ id: 1, name: 'Test User' })
    expect(result.current.login).toBeDefined()
  })

  it('should throw error when used outside provider', () => {
    // Capture error
    const { result } = renderHook(() => useAuth())
    expect(result.error).toEqual(
      Error('useAuth must be used within AuthProvider')
    )
  })
})

Truco: usa el parámetro wrapper para envolver el Provider.

Probar hooks async (obtención de datos)

Muchos hooks obtienen datos, por ejemplo:

// hooks/useFetch.ts
import { useState, useEffect } from 'react'

export function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true)
        const response = await fetch(url)
        if (!response.ok) throw new Error('Network error')
        const json = await response.json()
        setData(json)
      } catch (err) {
        setError(err as Error)
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [url])

  return { data, loading, error }
}

Probar hooks async requiere mockear fetch y esperar actualizaciones:

// hooks/useFetch.test.ts
import { renderHook, waitFor } from '@testing-library/react'
import { useFetch } from './useFetch'

describe('useFetch', () => {
  beforeEach(() => {
    // Reset fetch Mock before each test
    jest.resetAllMocks()
  })

  it('should fetch data successfully', async () => {
    const mockData = { id: 1, title: 'Test' }

    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve(mockData),
      })
    ) as jest.Mock

    const { result } = renderHook(() => useFetch('/api/data'))

    // Initial state: loading = true
    expect(result.current.loading).toBe(true)
    expect(result.current.data).toBeNull()

    // Wait for data to load
    await waitFor(() => {
      expect(result.current.loading).toBe(false)
    })

    expect(result.current.data).toEqual(mockData)
    expect(result.current.error).toBeNull()
  })

  it('should handle fetch error', async () => {
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: false,
      })
    ) as jest.Mock

    const { result } = renderHook(() => useFetch('/api/data'))

    await waitFor(() => {
      expect(result.current.loading).toBe(false)
    })

    expect(result.current.error).toBeTruthy()
    expect(result.current.error?.message).toBe('Network error')
    expect(result.current.data).toBeNull()
  })
})

Notas:

  • Usa waitFor para esperar operaciones async
  • Reinicia mocks en beforeEach para evitar interferencias
  • Prueba escenarios de éxito y de error

El núcleo: mockear dependencias, provocar cambios de estado y afirmar resultados. Con esos tres pasos puedes probar cualquier hook.

Guía completa de técnicas de mock

El mock es el alma de las pruebas. Sin él dependes de APIs, bases de datos y servicios externos: lento e inestable. Next.js tiene muchas piezas que hay que mockear; este capítulo explica cómo.

Mockear el router de Next.js (lo más habitual)

Los hooks de rutas (useRouter, usePathname, useSearchParams) no están disponibles en pruebas; hay que mockearlos.

Mock de useRouter (App Router):

// __mocks__/next/navigation.ts
export const useRouter = jest.fn()
export const usePathname = jest.fn()
export const useSearchParams = jest.fn()

Uso en el archivo de prueba:

import { useRouter } from 'next/navigation'

// Mock del comportamiento del router
jest.mock('next/navigation', () => ({
  useRouter: jest.fn(),
  usePathname: jest.fn(),
  useSearchParams: jest.fn(),
}))

describe('NavigationComponent', () => {
  it('should navigate to home on button click', () => {
    const pushMock = jest.fn()
    ;(useRouter as jest.Mock).mockReturnValue({
      push: pushMock,
      back: jest.fn(),
      forward: jest.fn(),
    })

    render(<NavigationComponent />)

    const button = screen.getByRole('button', { name: '回到首页' })
    fireEvent.click(button)

    expect(pushMock).toHaveBeenCalledWith('/')
  })
})

Mock de usePathname (ruta actual):

import { usePathname } from 'next/navigation'

jest.mock('next/navigation', () => ({
  usePathname: jest.fn(),
}))

describe('HeaderComponent', () => {
  it('should highlight active nav item', () => {
    ;(usePathname as jest.Mock).mockReturnValue('/about')

    render(<Header />)

    const aboutLink = screen.getByRole('link', { name: '关于' })
    expect(aboutLink).toHaveClass('active')
  })
})

Mockear el componente Image de Next.js

next/image también falla en pruebas porque depende del servicio de optimización de imágenes.

Opción 1: mockear como etiqueta img:

// __mocks__/next/image.tsx
const Image = ({ src, alt }: { src: string; alt: string }) => {
  return <img src={src} alt={alt} />
}

export default Image

Opción 2: mock global en jest.config.ts:

const config: Config = {
  // ... otra configuración
  moduleNameMapper: {
    '^next/image$': '<rootDir>/__mocks__/next/image.tsx',
  },
}

Así sustituyes automáticamente todo uso de next/image.

Mockear peticiones API (tres métodos)

Método 1: mock global de fetch (más simple):

global.fetch = jest.fn(() =>
  Promise.resolve({
    ok: true,
    json: () => Promise.resolve({ data: 'mock data' }),
  })
) as jest.Mock

Método 2: MSW (Mock Service Worker) (más potente):

npm install -D msw
// mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/posts', () => {
    return HttpResponse.json([
      { id: 1, title: 'Test Post' },
    ])
  }),

  http.post('/api/login', async ({ request }) => {
    const { email } = await request.json()
    if (email === '[email protected]') {
      return HttpResponse.json({ success: true })
    }
    return HttpResponse.json({ error: 'Invalid email' }, { status: 400 })
  }),
]
// mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)

Inicia el servidor mock en jest.setup.ts:

import '@testing-library/jest-dom'
import { server } from './mocks/server'

// Iniciar servidor mock antes de las pruebas
beforeAll(() => server.listen())

// Reiniciar handlers tras cada prueba
afterEach(() => server.resetHandlers())

// Cerrar servidor al terminar
afterAll(() => server.close())

MSW intercepta todas las peticiones sin escribir global.fetch en cada prueba.

Método 3: mock de axios (si usas axios):

npm install -D axios-mock-adapter
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'

const mock = new MockAdapter(axios)

describe('API Test', () => {
  afterEach(() => {
    mock.reset()
  })

  it('should fetch posts', async () => {
    mock.onGet('/api/posts').reply(200, [{ id: 1, title: 'Test' }])

    const response = await axios.get('/api/posts')
    expect(response.data).toHaveLength(1)
  })
})

Mockear variables de entorno

Las variables de entorno de Next.js también deben mockearse en pruebas.

Método 1: asignar process.env directamente:

describe('Config Test', () => {
  const originalEnv = process.env

  beforeEach(() => {
    jest.resetModules()
    process.env = { ...originalEnv }
  })

  afterEach(() => {
    process.env = originalEnv
  })

  it('should use API URL from env', () => {
    process.env.NEXT_PUBLIC_API_URL = 'https://test-api.com'

    const { getApiUrl } = require('@/lib/config')
    expect(getApiUrl()).toBe('https://test-api.com')
  })
})

Método 2: archivo .env.test:

Crea .env.test; next/jest lo cargará automáticamente:

NEXT_PUBLIC_API_URL=https://test-api.com
DATABASE_URL=postgresql://test:test@localhost:5432/test

Mockear módulos de terceros (ejemplo Prisma)

Con Prisma no querrás conectar a una base de datos real en las pruebas.

Mock del cliente Prisma:

// __mocks__/prisma.ts
export const prisma = {
  user: {
    findMany: jest.fn(),
    findUnique: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
  },
  post: {
    findMany: jest.fn(),
    create: jest.fn(),
  },
}

Uso en pruebas:

import { prisma } from '@/lib/prisma'

jest.mock('@/lib/prisma', () => ({
  prisma: {
    user: {
      findUnique: jest.fn(),
    },
  },
}))

describe('getUserById', () => {
  it('should return user', async () => {
    const mockUser = { id: 1, name: 'Test User' }
    ;(prisma.user.findUnique as jest.Mock).mockResolvedValue(mockUser)

    const result = await getUserById(1)
    expect(result).toEqual(mockUser)
    expect(prisma.user.findUnique).toHaveBeenCalledWith({
      where: { id: 1 },
    })
  })
})

Errores habituales de mock y soluciones

Problema 1: Cannot find module 'next/router'

Causa: no se mockeó el módulo de rutas.

Solución: añade jest.mock('next/navigation') al inicio del archivo.

Problema 2: el mock no funciona

Causa: jest.mock en posición incorrecta; debe ir arriba (tras los imports).

import { useRouter } from 'next/navigation'

// Mock obligatorio aquí
jest.mock('next/navigation')

describe('Test', () => {
  // ...
})

Problema 3: no lee variables de entorno

Causa: las pruebas no cargan .env.

Solución: usa next/jest en jest.config.ts; carga variables automáticamente.

Con estos mocks podrás probar casi todas las piezas especiales de Next.js.

Resolución de problemas frecuentes

Configurar Jest implica errores; la buena noticia es que el 90% son los mismos y tienen solución estándar.

Error 1: Cannot use import statement outside a module

Error completo:

SyntaxError: Cannot use import statement outside a module

Causa: Jest no admite ES Modules por defecto y tu código o dependencias usan import/export.

Solución: añade en jest.config.ts:

const config: Config = {
  // ... otra configuración
  extensionsToTreatAsEsm: ['.ts', '.tsx'],
  transformIgnorePatterns: [
    'node_modules/(?!(module-that-uses-esm)/)',
  ],
}

Si el problema es un paquete npm (nanoid, uuid, etc.), añádelo a las excepciones de transformIgnorePatterns:

transformIgnorePatterns: [
  'node_modules/(?!(nanoid|uuid)/)',
]

Error 2: Unexpected token ‘export’

Causa: algún archivo no se transforma con Jest.

Solución: revisa transform y transformIgnorePatterns; asegura el envoltorio next/jest:

import nextJest from 'next/jest'

const createJestConfig = nextJest({ dir: './' })

export default createJestConfig(config)

next/jest transforma TypeScript y JSX automáticamente.

Error 3: Cannot find module ’@/components/…’

Causa: Jest no reconoce el alias @/.

Solución: configura moduleNameMapper en jest.config.ts:

moduleNameMapper: {
  '^@/components/(.*)$': '<rootDir>/components/$1',
  '^@/lib/(.*)$': '<rootDir>/lib/$1',
  '^@/(.*)$': '<rootDir>/$1',
}

Debe coincidir con paths en tsconfig.json.

Error 4: Objects are not valid as a React child

Causa: probaste un Server Component async; Jest no lo admite.

Solución:

  1. Extrae la lógica async en funciones puras y pruébala aparte
  2. O usa pruebas E2E (Playwright, Cypress)

No fuerces Jest con Server Components; ninguna herramienta lo hace todo.

Error 5: advertencia act(…)

Error completo:

Warning: An update to Component inside a test was not wrapped in act(...).

Causa: hay actualizaciones async (useEffect, setTimeout) y la prueba no esperó.

Solución: usa waitFor o act para envolver operaciones async:

import { waitFor } from '@testing-library/react'

it('should update state', async () => {
  render(<Component />)

  await waitFor(() => {
    expect(screen.getByText('Updated')).toBeInTheDocument()
  })
})

O usa act:

import { act } from '@testing-library/react'

it('should trigger callback', async () => {
  await act(async () => {
    render(<Component />)
  })
})

¿Las pruebas van lentas? Prueba estas optimizaciones

  1. Ejecutar pruebas en paralelo:
{
  "scripts": {
    "test": "jest --maxWorkers=4"
  }
}
  1. Probar solo archivos modificados:
npm test -- --onlyChanged
  1. Desactivar cobertura (al depurar):
{
  "scripts": {
    "test:fast": "jest --no-coverage"
  }
}
  1. Omitir pruebas lentas con test.skip:
describe.skip('Slow Tests', () => {
  // Estas pruebas se omiten
})

Lista de diagnóstico

Ante un error, revisa en este orden:

  1. ✅ ¿jest.config.ts usa next/jest como envoltorio?
  2. ✅ ¿jest.setup.ts importa @testing-library/jest-dom?
  3. ✅ ¿Los alias coinciden con tsconfig.json?
  4. ✅ ¿Mockeaste next/navigation y next/image?
  5. ✅ ¿Las operaciones async usan waitFor o act?
  6. ✅ ¿Las versiones de dependencias son compatibles (React 19 y Jest)?

Seguir esta lista resuelve la mayoría de problemas.

Conclusión

Configurar pruebas es tedioso, pero en marcha eleva mucho la calidad del código.

Al principio pensé que perdía tiempo: cinco minutos en una función, diez en pruebas. Luego descubrí que refactorizar y corregir bugs es mucho más tranquilo: todo verde, confianza; algo rojo, sabes qué rompiste.

Mis consejos:

  • No esperes a que el proyecto sea enorme. Desde ahora, una prueba por función nueva.
  • No persigas el 100% de cobertura. Cubre bien la lógica crítica y lo propenso a fallos.
  • Si no puedes probar Server Components, no fuerces: extrae lógica o usa E2E.
  • Ante errores, no entres en pánico: usa la lista de diagnóstico.

Guarda estos archivos de configuración; en el próximo proyecto cópialos y en diez minutos tendrás pruebas. Menos bugs, mejor código.

Si tienes problemas al configurar o escribir pruebas, comenta. Yo también pasé por esos tropiezos y ayudo en lo que pueda.

Flujo completo de configuración del entorno de pruebas Jest en Next.js

Pasos detallados para configurar desde cero el entorno de pruebas Next.js 15 + Jest + React Testing Library

⏱️ Estimated time: 15 min

  1. 1

    Step 1: Instalar dependencias de prueba

    Instala Jest y el ecosistema de React Testing Library:

    • npm install -D jest jest-environment-jsdom
    • npm install -D @testing-library/react @testing-library/dom @testing-library/jest-dom
    • npm install -D ts-node @types/jest (necesario en proyectos TypeScript)

    Descripción de paquetes:
    • jest: núcleo del framework de pruebas
    • jest-environment-jsdom: simula el entorno DOM del navegador
    • @testing-library/react: utilidades para probar componentes React
    • @testing-library/jest-dom: aserciones extendidas (como toBeInTheDocument)

    Tras instalar todo de una vez, no ejecutes pruebas todavía; primero crea los archivos de configuración.
  2. 2

    Step 2: Crear archivo de configuración de Jest

    Crea jest.config.ts en la raíz del proyecto y usa next/jest para manejar automáticamente las características de Next.js:

    ```typescript
    import type { Config } from 'jest'
    import nextJest from 'next/jest'

    const createJestConfig = nextJest({ dir: './' })

    const config: Config = {
    coverageProvider: 'v8',
    testEnvironment: 'jsdom',
    setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
    moduleNameMapper: {
    '^@/components/(.*)$': '<rootDir>/components/$1',
    '^@/lib/(.*)$': '<rootDir>/lib/$1',
    },
    }

    export default createJestConfig(config)
    ```

    next/jest maneja automáticamente: mocks de CSS/imágenes, carga de variables de entorno, transformación de TypeScript y exclusión de node_modules.
  3. 3

    Step 3: Crear configuración de arranque de Jest

    Crea jest.setup.ts en la raíz del proyecto e importa las extensiones de Jest DOM:

    ```typescript
    import '@testing-library/jest-dom'
    ```

    Esta línea permite usar aserciones extendidas:
    • expect(element).toBeInTheDocument()
    • expect(element).toHaveClass('active')
    • expect(element).toBeVisible()
    • expect(element).toHaveTextContent('text')

    Sin importar este archivo, los métodos anteriores lanzarán errores not a function.
  4. 4

    Step 4: Configurar scripts de prueba

    Añade comandos de prueba en package.json:

    ```json
    {
    "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
    }
    }
    ```

    Tres comandos y sus usos:
    • npm test: ejecuta todas las pruebas (CI/CD)
    • npm run test:watch: modo watch, prueba automática al cambiar archivos (desarrollo)
    • npm run test:coverage: genera informe de cobertura de código
  5. 5

    Step 5: Crear prueba de ejemplo para verificar la configuración

    Crea __tests__/example.test.ts para verificar que el entorno está configurado:

    ```typescript
    describe('Example Test', () => {
    it('should pass basic assertion', () => {
    expect(1 + 1).toBe(2)
    })
    })
    ```

    Ejecuta npm test; ver PASS en verde indica éxito.

    Si hay errores, revisa en orden:
    1. ¿jest.config.ts usa createJestConfig como envoltorio?
    2. ¿jest.setup.ts importa correctamente?
    3. ¿Los scripts de package.json son correctos?
    4. ¿Los alias de rutas coinciden con tsconfig.json?

FAQ

¿Por qué hay que envolver la configuración con next/jest?
next/jest es la herramienta oficial de configuración de Jest de Next.js y maneja automáticamente muchas configuraciones complejas:

• Mock automático de archivos CSS e imágenes (evita errores en pruebas)
• Carga automática de variables de entorno .env
• Transformación automática de TypeScript y JSX
• Exclusión automática de node_modules y .next
• Reglas de transformación del compilador de Next.js

Sin next/jest tendrías que configurar todo manualmente, lo cual es tedioso y propenso a errores. La documentación oficial recomienda usar createJestConfig.
¿Se pueden probar Server Components con Jest?
Parcialmente, pero con limitaciones:

• Server Components síncronos: se pueden probar con normalidad
• Server Components async: Jest no los admite y lanza Objects are not valid as a React child

Enfoque recomendado:
1. Extrae la lógica async en funciones puras y prueba la obtención de datos por separado
2. Deja que los Server Components solo rendericen de forma simple, sin lógica compleja
3. Usa Playwright o Cypress para pruebas E2E del flujo completo

No fuerces Jest para todos los Server Components; elegir la herramienta adecuada importa más.
¿Cómo hacer mock de useRouter de Next.js en las pruebas?
Los hooks de rutas del App Router de Next.js deben mockearse para probar:

```typescript
import { useRouter } from 'next/navigation'

jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
usePathname: jest.fn(),
useSearchParams: jest.fn(),
}))

const pushMock = jest.fn()
;(useRouter as jest.Mock).mockReturnValue({
push: pushMock,
back: jest.fn(),
forward: jest.fn(),
})
```

jest.mock debe ir al inicio del archivo de prueba (después de los imports), no dentro de describe o it.
¿Por qué aparece el error Cannot use import statement outside a module?
Ocurre porque Jest no admite ES Modules por defecto. Dos soluciones:

Opción 1: configurar transformIgnorePatterns (recomendado)
```typescript
const config: Config = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
transformIgnorePatterns: [
'node_modules/(?!(nanoid|uuid)/)',
],
}
```

Opción 2: asegurar que next/jest envuelve la configuración
```typescript
import nextJest from 'next/jest'
const createJestConfig = nextJest({ dir: './' })
export default createJestConfig(config)
```

En el 90% de los casos, algún paquete npm usa ESM; añádelo a la lista de excepciones de transformIgnorePatterns.
¿Cómo hacer mock de peticiones API? ¿Qué método se recomienda?
Tres métodos habituales, de menor a mayor complejidad:

Método 1: mock global de fetch (más simple, proyectos pequeños)
```typescript
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ data: 'test' }),
})
) as jest.Mock
```

Método 2: MSW (Mock Service Worker, recomendado para proyectos medianos/grandes)
• Intercepta todas las peticiones de red
• Soporta lógica compleja de request/response
• No necesitas escribir mock en cada prueba
• Instalación: npm install -D msw

Método 3: axios-mock-adapter (si usas axios)
• Biblioteca específica para axios
• API sencilla

Recomendamos MSW por acercarse más a peticiones reales y ser muy reutilizable en equipos.
¿Las pruebas van lentas, cómo optimizarlas?
Cuatro métodos prácticos:

1. Ejecutar pruebas en paralelo (más efectivo)
```json
{ "scripts": { "test": "jest --maxWorkers=4" } }
```

2. Probar solo archivos modificados
```bash
npm test -- --onlyChanged
```

3. Desactivar cobertura (al depurar)
```json
{ "scripts": { "test:fast": "jest --no-coverage" } }
```

4. Saltar pruebas lentas
```typescript
describe.skip('Slow E2E Tests', () => {})
```

En desarrollo usa --onlyChanged y --no-coverage; en CI/CD ejecuta la suite completa.
Configuré el alias @/ pero las pruebas siguen fallando, ¿qué hago?
Asegura que jest.config.ts y tsconfig.json tengan la misma configuración de rutas:

tsconfig.json:
```json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"]
}
}
}
```

jest.config.ts:
```typescript
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/lib/(.*)$': '<rootDir>/lib/$1',
}
```

Puntos clave: tsconfig usa rutas relativas; jest.config usa <rootDir>; los comodines deben coincidir. Reinicia las pruebas tras modificar.

17 min de lectura · Publicado el: 7 ene 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog