Estrategias de mocking para APIs y servicios externos con Jest

🧠 Conceptos clave

  • Mocking es una técnica para simular el comportamiento de dependencias externas durante los tests, evitando llamadas reales a APIs, bases de datos o servicios de terceros.
  • Jest ofrece utilidades integradas para crear mocks, espías y sustituciones dinámicas de módulos.
  • El objetivo: aislar la lógica interna del código sin depender de factores externos (latencia, credenciales, red).
  • Se usa tanto en Unit test (mocks simples) como en integration test (mocks más realistas con herramientas como MSW Mocks service worker).

⚙️ Tipos de mocks en Jest

Tipo Descripción Uso recomendado
Manual mocks Definidos en __mocks__ APIs o SDKs externos
Automáticos Generados por jest.mock() Dependencias pequeñas o funciones internas
Mock functions Con jest.fn() o jest.spyOn() Lógica interna o callbacks
Service Workers Con MSW Mocks service worker Pruebas de red realistas (front o backend)

🧩 Ejemplo 1: Mock de fetch global

// src/api/users.ts
export async function getUsers() {
  const response = await fetch('https://api.example.com/users')
  if (!response.ok) throw new Error('Error al obtener usuarios')
  return response.json()
}

`

// src/api/users.test.ts
import { getUsers } from './users'

describe('getUsers', () => {
  beforeEach(() => {
    global.fetch = vi.fn()
  })

  test('retorna lista de usuarios', async () => {
    const fakeData = [{ id: 1, name: 'Eduardo' }]
    ;(fetch as jest.Mock).mockResolvedValueOnce({
      ok: true,
      json: async () => fakeData,
    })

    const data = await getUsers()
    expect(data).toEqual(fakeData)
    expect(fetch).toHaveBeenCalledWith('https://api.example.com/users')
  })

  test('lanza error si la API falla', async () => {
    ;(fetch as jest.Mock).mockResolvedValueOnce({ ok: false })
    await expect(getUsers()).rejects.toThrow('Error al obtener usuarios')
  })
})

✅ Ideal para servicios basados en fetch o window.fetch en entornos frontend.


🧩 Ejemplo 2: Mock de axios

// src/api/login.ts
import axios from 'axios'
export const login = async (user: string, pass: string) => {
  const { data } = await axios.post('/auth/login', { user, pass })
  return data.token
}
// src/api/login.test.ts
import axios from 'axios'
import { login } from './login'
vi.mock('axios')

test('devuelve token al hacer login', async () => {
  ;(axios.post as jest.Mock).mockResolvedValueOnce({ data: { token: 'abc123' } })
  const token = await login('admin', '1234')
  expect(token).toBe('abc123')
  expect(axios.post).toHaveBeenCalledWith('/auth/login', { user: 'admin', pass: '1234' })
})

💡 vi.mock o jest.mock interceptan las importaciones y permiten sustituir módulos enteros.


🧠 Ejemplo 3: Mock manual con __mocks__

Estructura:

src/
├─ api/
│  ├─ payments.ts
│  └─ __mocks__/payments.ts
// src/api/payments.ts
export const getPaymentStatus = async (id: string) => {
  const res = await fetch(`/payments/${id}`)
  return res.json()
}
// src/api/__mocks__/payments.ts
export const getPaymentStatus = vi.fn().mockResolvedValue({ status: 'mocked-paid' })
// src/tests/payment.test.ts
import { getPaymentStatus } from '../api/payments'
vi.mock('../api/payments')

test('usa el mock manual correctamente', async () => {
  const res = await getPaymentStatus('123')
  expect(res.status).toBe('mocked-paid')
})

✅ Los mocks manuales son ideales para reutilizar respuestas simuladas complejas o centralizar comportamientos de APIs externas.


🧩 Ejemplo 4: Mock con MSW Mocks service worker

MSW permite interceptar peticiones fetch o XMLHttpRequest sin modificar el código original.

Instalación

npm install msw --save-dev

Handlers (src/mocks/handlers.ts)

import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('https://api.example.com/users', () =>
    HttpResponse.json([{ id: 1, name: 'Mocked User' }])
  ),
]

Setup (src/mocks/server.ts)

import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

Test

import { getUsers } from '../api/users'
import { server } from '../mocks/server'

test('retorna usuario simulado desde MSW', async () => {
  const users = await getUsers()
  expect(users[0].name).toBe('Mocked User')
})

🧩 MSW es la opción más realista: intercepta las peticiones HTTP como lo haría el navegador o Node, sin necesidad de reescribir código ni usar vi.mock.


🧠 Ejemplo 5: Mock dinámico para errores controlados

import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
import { getUsers } from '../api/users'

test('maneja errores del servidor', async () => {
  server.use(
    http.get('https://api.example.com/users', () =>
      HttpResponse.json({ message: 'Internal Error' }, { status: 500 })
    )
  )

  await expect(getUsers()).rejects.toThrow()
})

🧩 Ejemplo 6: Mock de módulos del sistema (ej. fs, path)

// src/utils/file.ts
import fs from 'fs'
export const readConfig = () => fs.readFileSync('config.json', 'utf8')
// src/utils/file.test.ts
import fs from 'fs'
import { readConfig } from './file'
vi.mock('fs')

test('lee el archivo simulado', () => {
  ;(fs.readFileSync as jest.Mock).mockReturnValue('{"env":"test"}')
  expect(readConfig()).toContain('test')
})

💡 Los mocks de módulos internos permiten testear sin acceder al sistema de archivos, red o procesos reales.


💡 Buenas prácticas

  • Usa mocks automáticos (jest.mock) para dependencias simples.
  • Centraliza respuestas falsas en __mocks__ o fixtures.
  • Usa MSW Mocks service worker para tests de integración y entorno CICD.
  • Limpia los mocks entre pruebas (jest.clearAllMocks()).
  • Evita mocks excesivos: solo simula dependencias externas.
  • Usa mockRejectedValue para simular errores o timeouts.
  • Define tipados correctos en mocks (as jest.Mock o Mocked<T>).
  • Simula latencia con await waitFor() o mockImplementation(async () => ...).

🧠 Depuración y errores comunes

Problema Causa Solución
“fetch is not defined” Jest corre en Node sin fetch global Instala whatwg-fetch o usa MSW
Mocks persistentes entre tests Falta jest.resetAllMocks() Añadir a beforeEach()
“Module not mocked” Ruta incorrecta en jest.mock() Usa paths relativos correctos
Mock incompleto Falta default export Usa vi.mock('axios', () => ({ default: { ... } }))
Llamadas reales en CI Falta setup global de MSW Importar server.listen() en setup global

📊 Integración con CICD

name: Jest API Mocks
on: [push, pull_request]

jobs:
  test-api:
    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

📈 Se recomienda almacenar las respuestas mock en JSONs versionados (/fixtures) y validarlas con SonarQube para consistencia.


📚 Recursos recomendados