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 (
);
}
// 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 (
);
}
// 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
Aprende Más
📚 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