TechLead

Testing de Integración

Testea cómo los componentes trabajan juntos: integración de API, testing de base de datos e interacciones de componentes

¿Qué es el Testing de Integración?

El testing de integración verifica que diferentes módulos, servicios o componentes de tu aplicación funcionen correctamente juntos. A diferencia de los tests unitarios que aíslan funciones individuales, los tests de integración verifican las interacciones entre múltiples partes de tu sistema.

🎯 Objetivos del Test de Integración:

Detectar bugs que ocurren cuando los componentes interactúan, verificar contratos de API, testear operaciones de base de datos y asegurar el flujo apropiado de datos entre módulos.

Tipos de Tests de Integración

🔌 Integración de API

Testeando comunicación frontend-backend

  • • Peticiones y respuestas HTTP
  • • Flujos de autenticación
  • • Serialización de datos
  • • Manejo de errores

🗄️ Integración de Base de Datos

Testeando operaciones de base de datos

  • • Operaciones CRUD
  • • Transacciones
  • • Relaciones y joins
  • • Migraciones

🧩 Integración de Componentes

Testeando árboles de componentes React

  • • Comunicación padre-hijo
  • • Context y gestión de estado
  • • Propagación de eventos
  • • Envíos de formularios

🔗 Integración de Servicios

Testeando interacciones de capa de servicio

  • • Flujos de lógica de negocio
  • • Llamadas a API externas
  • • Colas de mensajes
  • • Interacciones de caché

Testeando Integración de API

// api.js - Cliente de API
export class API {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  async get(endpoint) {
    const response = await fetch(this.baseURL + endpoint);
    if (!response.ok) {
      throw new Error('Petición falló: ' + response.status);
    }
    return response.json();
  }

  async post(endpoint, data) {
    const response = await fetch(this.baseURL + endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    if (!response.ok) {
      throw new Error('Petición falló: ' + response.status);
    }
    return response.json();
  }
}
// api.integration.test.js - Test de integración con HTTP real
import { API } from './api';
import { setupServer } from 'msw/node';
import { rest } from 'msw';

// Mock Service Worker - intercepta peticiones HTTP reales
const server = setupServer(
  rest.get('https://api.example.com/users', (req, res, ctx) => {
    return res(
      ctx.json([
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' },
      ])
    );
  }),

  rest.post('https://api.example.com/users', async (req, res, ctx) => {
    const body = await req.json();
    return res(
      ctx.json({
        id: 3,
        ...body,
      })
    );
  }),

  rest.get('https://api.example.com/error', (req, res, ctx) => {
    return res(ctx.status(500));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('Integración de API', () => {
  test('obtiene usuarios exitosamente', async () => {
    const api = new API('https://api.example.com');
    const users = await api.get('/users');

    expect(users).toHaveLength(2);
    expect(users[0].name).toBe('Alice');
  });

  test('crea usuario exitosamente', async () => {
    const api = new API('https://api.example.com');
    const newUser = await api.post('/users', {
      name: 'Charlie',
      email: 'charlie@example.com',
    });

    expect(newUser.id).toBe(3);
    expect(newUser.name).toBe('Charlie');
  });

  test('maneja errores del servidor', async () => {
    const api = new API('https://api.example.com');
    
    await expect(api.get('/error')).rejects.toThrow('Petición falló: 500');
  });
});

Testeando Integración de Componentes React

// TodoList.jsx - Componente padre
import { useState } from 'react';
import TodoForm from './TodoForm';
import TodoItem from './TodoItem';

export function TodoList() {
  const [todos, setTodos] = useState([]);

  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text, done: false }]);
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    

Mis Tareas

{todos.map(todo => ( ))}
{todos.length === 0 &&

No hay tareas aún

}
); } // TodoForm.jsx - Componente hijo export function TodoForm({ onAdd }) { const [input, setInput] = useState(''); const handleSubmit = (e) => { e.preventDefault(); if (input.trim()) { onAdd(input); setInput(''); } }; return (
setInput(e.target.value)} placeholder="Agregar una tarea..." />
); } // TodoItem.jsx - Componente hijo export function TodoItem({ todo, onToggle, onDelete }) { return (
onToggle(todo.id)} /> {todo.text}
); }
// TodoList.integration.test.jsx - Test de integración de componentes
import { render, screen, fireEvent, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TodoList } from './TodoList';

describe('Integración de TodoList', () => {
  test('usuario puede agregar, marcar y eliminar tareas', async () => {
    const user = userEvent.setup();
    render();

    // Inicialmente vacío
    expect(screen.getByText('No hay tareas aún')).toBeInTheDocument();

    // Agregar primera tarea
    const input = screen.getByPlaceholderText('Agregar una tarea...');
    await user.type(input, 'Comprar leche');
    await user.click(screen.getByText('Agregar'));

    // Verificar que se agregó la tarea
    expect(screen.getByText('Comprar leche')).toBeInTheDocument();
    expect(screen.queryByText('No hay tareas aún')).not.toBeInTheDocument();
    expect(input).toHaveValue(''); // Input limpio

    // Agregar segunda tarea
    await user.type(input, 'Pasear perro');
    await user.click(screen.getByText('Agregar'));
    expect(screen.getByText('Pasear perro')).toBeInTheDocument();

    // Marcar primera tarea
    const firstCheckbox = screen.getAllByRole('checkbox')[0];
    await user.click(firstCheckbox);
    
    const comprarLeche = screen.getByText('Comprar leche');
    expect(comprarLeche).toHaveStyle({ textDecoration: 'line-through' });

    // Eliminar segunda tarea
    const deleteButtons = screen.getAllByText('Eliminar');
    await user.click(deleteButtons[1]);
    
    expect(screen.queryByText('Pasear perro')).not.toBeInTheDocument();
    expect(screen.getByText('Comprar leche')).toBeInTheDocument();
  });

  test('no agrega tareas vacías', async () => {
    const user = userEvent.setup();
    render();

    const addButton = screen.getByText('Agregar');
    
    // Intentar agregar vacío
    await user.click(addButton);
    expect(screen.getByText('No hay tareas aún')).toBeInTheDocument();

    // Intentar agregar solo espacios
    await user.type(screen.getByPlaceholderText('Agregar una tarea...'), '   ');
    await user.click(addButton);
    expect(screen.getByText('No hay tareas aún')).toBeInTheDocument();
  });

  test('mantiene estado de tareas a través de múltiples operaciones', async () => {
    const user = userEvent.setup();
    render();

    // Agregar tres tareas
    const input = screen.getByPlaceholderText('Agregar una tarea...');
    
    await user.type(input, 'Primera');
    await user.click(screen.getByText('Agregar'));
    
    await user.type(input, 'Segunda');
    await user.click(screen.getByText('Agregar'));
    
    await user.type(input, 'Tercera');
    await user.click(screen.getByText('Agregar'));

    // Marcar primera y tercera
    const checkboxes = screen.getAllByRole('checkbox');
    await user.click(checkboxes[0]);
    await user.click(checkboxes[2]);

    // Eliminar segunda
    const deleteButtons = screen.getAllByText('Eliminar');
    await user.click(deleteButtons[1]);

    // Verificar estado final
    expect(screen.getByText('Primera')).toHaveStyle({ 
      textDecoration: 'line-through' 
    });
    expect(screen.queryByText('Segunda')).not.toBeInTheDocument();
    expect(screen.getByText('Tercera')).toHaveStyle({ 
      textDecoration: 'line-through' 
    });
  });
});

Testeando con Context y Gestión de Estado

// AuthContext.jsx
import { createContext, useContext, useState } from 'react';

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (username, password) => {
    // Login simplificado
    if (password === 'correct') {
      setUser({ username });
      return true;
    }
    return false;
  };

  const logout = () => {
    setUser(null);
  };

  return (
    
      {children}
    
  );
}

export const useAuth = () => useContext(AuthContext);

// Protected.jsx - Componente usando context
export function Protected() {
  const { user, logout } = useAuth();

  if (!user) {
    return 
Por favor inicia sesión
; } return (

¡Bienvenido {user.username}!

); } // LoginForm.jsx export function LoginForm() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const { login } = useAuth(); const handleSubmit = (e) => { e.preventDefault(); const success = login(username, password); if (!success) { setError('Credenciales inválidas'); } }; return (
setUsername(e.target.value)} /> setPassword(e.target.value)} /> {error &&
{error}
}
); }
// Auth.integration.test.jsx - Testeando con context
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AuthProvider } from './AuthContext';
import { LoginForm } from './LoginForm';
import { Protected } from './Protected';

function App() {
  return (
    
      
      
    
  );
}

describe('Integración de Autenticación', () => {
  test('usuario puede iniciar sesión y ver contenido protegido', async () => {
    const user = userEvent.setup();
    render();

    // Inicialmente sin sesión
    expect(screen.getByText('Por favor inicia sesión')).toBeInTheDocument();

    // Llenar formulario de login
    await user.type(screen.getByPlaceholderText('Usuario'), 'alice');
    await user.type(screen.getByPlaceholderText('Contraseña'), 'correct');
    await user.click(screen.getByText('Iniciar Sesión'));

    // Verificar sesión iniciada
    await waitFor(() => {
      expect(screen.getByText('¡Bienvenido alice!')).toBeInTheDocument();
    });
    expect(screen.queryByText('Por favor inicia sesión')).not.toBeInTheDocument();
  });

  test('muestra error en login inválido', async () => {
    const user = userEvent.setup();
    render();

    await user.type(screen.getByPlaceholderText('Usuario'), 'alice');
    await user.type(screen.getByPlaceholderText('Contraseña'), 'wrong');
    await user.click(screen.getByText('Iniciar Sesión'));

    expect(await screen.findByRole('alert')).toHaveTextContent(
      'Credenciales inválidas'
    );
    expect(screen.getByText('Por favor inicia sesión')).toBeInTheDocument();
  });

  test('usuario puede cerrar sesión', async () => {
    const user = userEvent.setup();
    render();

    // Iniciar sesión primero
    await user.type(screen.getByPlaceholderText('Usuario'), 'alice');
    await user.type(screen.getByPlaceholderText('Contraseña'), 'correct');
    await user.click(screen.getByText('Iniciar Sesión'));

    await waitFor(() => {
      expect(screen.getByText('¡Bienvenido alice!')).toBeInTheDocument();
    });

    // Cerrar sesión
    await user.click(screen.getByText('Cerrar Sesión'));

    expect(screen.getByText('Por favor inicia sesión')).toBeInTheDocument();
  });
});

Testing de Integración de Base de Datos

// userRepository.js - Capa de base de datos
export class UserRepository {
  constructor(db) {
    this.db = db;
  }

  async create(user) {
    const result = await this.db.query(
      'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
      [user.name, user.email]
    );
    return result.rows[0];
  }

  async findById(id) {
    const result = await this.db.query(
      'SELECT * FROM users WHERE id = $1',
      [id]
    );
    return result.rows[0] || null;
  }

  async findByEmail(email) {
    const result = await this.db.query(
      'SELECT * FROM users WHERE email = $1',
      [email]
    );
    return result.rows[0] || null;
  }

  async update(id, updates) {
    const result = await this.db.query(
      'UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *',
      [updates.name, updates.email, id]
    );
    return result.rows[0] || null;
  }

  async delete(id) {
    await this.db.query('DELETE FROM users WHERE id = $1', [id]);
  }
}
// userRepository.integration.test.js - Test de integración de base de datos
import { UserRepository } from './userRepository';
import { setupTestDatabase, cleanupTestDatabase } from './test-utils';

describe('Integración de UserRepository', () => {
  let db;
  let repo;

  beforeAll(async () => {
    // Crear conexión de base de datos de test
    db = await setupTestDatabase();
    repo = new UserRepository(db);
  });

  afterAll(async () => {
    await cleanupTestDatabase(db);
  });

  beforeEach(async () => {
    // Limpiar base de datos antes de cada test
    await db.query('DELETE FROM users');
  });

  test('crea y obtiene usuario', async () => {
    const user = await repo.create({
      name: 'Alice',
      email: 'alice@example.com',
    });

    expect(user.id).toBeDefined();
    expect(user.name).toBe('Alice');
    expect(user.email).toBe('alice@example.com');

    const found = await repo.findById(user.id);
    expect(found).toEqual(user);
  });

  test('encuentra usuario por email', async () => {
    await repo.create({
      name: 'Bob',
      email: 'bob@example.com',
    });

    const found = await repo.findByEmail('bob@example.com');
    expect(found.name).toBe('Bob');
  });

  test('devuelve null para usuario no existente', async () => {
    const found = await repo.findById(99999);
    expect(found).toBeNull();
  });

  test('actualiza usuario', async () => {
    const user = await repo.create({
      name: 'Charlie',
      email: 'charlie@example.com',
    });

    const updated = await repo.update(user.id, {
      name: 'Charles',
      email: 'charles@example.com',
    });

    expect(updated.name).toBe('Charles');
    expect(updated.email).toBe('charles@example.com');

    const found = await repo.findById(user.id);
    expect(found.name).toBe('Charles');
  });

  test('elimina usuario', async () => {
    const user = await repo.create({
      name: 'David',
      email: 'david@example.com',
    });

    await repo.delete(user.id);

    const found = await repo.findById(user.id);
    expect(found).toBeNull();
  });

  test('maneja restricción de email duplicado', async () => {
    await repo.create({
      name: 'Eve',
      email: 'eve@example.com',
    });

    await expect(
      repo.create({
        name: 'Evil Eve',
        email: 'eve@example.com',
      })
    ).rejects.toThrow();
  });
});

⚠️ Desafíos del Testing de Integración

  • Más lentos que tests unitarios: Pueden involucrar bases de datos reales, APIs o sistema de archivos
  • Requieren más configuración: Necesitas configurar entornos de test, sembrar datos
  • Pueden ser inestables: Problemas de red, timing, condiciones de carrera
  • Más difíciles de depurar: Más partes móviles significan más lugares donde pueden esconderse bugs
  • Dependencias de entorno: Pueden necesitar Docker, bases de datos de test, etc.

💡 Mejores Prácticas de Testing de Integración

  • ✓ Usa bases de datos de test o bases de datos en memoria para tests de BD
  • ✓ Limpia datos entre tests para asegurar independencia
  • ✓ Simula servicios externos que no controlas (APIs de terceros)
  • ✓ Testea los puntos de integración, no detalles de implementación
  • ✓ Mantén tests de integración enfocados en rutas críticas
  • ✓ Usa herramientas como MSW para mocking de API
  • ✓ Ejecuta tests de integración en pipeline CI/CD
  • ✓ Balancea velocidad con realismo - no simules todo

📚 Más Temas de Testing

Explora los 6 temas de testing para construir una comprensión completa del testing de software.

Ver Todos los Temas