Las bases de datos son la fundación de la mayoría de las aplicaciones, almacenando datos críticos del negocio y potenciando funcionalidad central. Sin embargo, las pruebas de bases de datos a menudo reciben menos atención que las pruebas de aplicaciones, llevando a problemas de producción que van desde corrupción de datos hasta degradación catastrófica del rendimiento. Esta guía completa cubre los aspectos esenciales de las pruebas de bases de datos—desde asegurar la integridad de datos hasta validar migraciones complejas—equipando a los ingenieros de QA con estrategias prácticas para validación exhaustiva de bases de datos.
Entendiendo Database Testing
Database testing valida la capa de datos de aplicaciones, enfocándose en:
- Integridad y consistencia de datos
- Corrección del esquema
- Comportamiento de transacciones (propiedades ACID)
- Rendimiento bajo carga
- Seguridad y control de acceso
- Mecanismos de backup y recuperación
- Procesos de migración y actualización
Pruebas de Integridad y Consistencia de Datos
La integridad de datos asegura que los datos permanezcan precisos, completos y consistentes a lo largo de su ciclo de vida.
Tipos de Integridad de Datos
1. Integridad de Entidad
La integridad de entidad requiere que cada tabla tenga una clave primaria y que la clave primaria sea única y no nula.
-- Prueba 1: Verificar que existe clave primaria
SELECT
table_name,
COUNT(*) as pk_count
FROM information_schema.table_constraints
WHERE constraint_type = 'PRIMARY KEY'
AND table_schema = 'your_database'
GROUP BY table_name
HAVING pk_count = 0;
-- Esperado: Sin resultados (cada tabla debería tener PK)
-- Prueba 2: Verificar sin claves primarias NULL
SELECT * FROM users WHERE id IS NULL;
-- Esperado: 0 filas
-- Prueba 3: Verificar sin claves primarias duplicadas
SELECT id, COUNT(*) as count
FROM users
GROUP BY id
HAVING count > 1;
-- Esperado: 0 filas
2. Integridad Referencial
La integridad referencial asegura que las relaciones entre tablas permanezcan consistentes.
-- Prueba: Encontrar registros huérfanos de órdenes
SELECT o.*
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.id IS NULL;
-- Esperado: 0 filas (todas las órdenes deberían tener usuarios válidos)
-- Probar comportamientos CASCADE
BEGIN;
INSERT INTO users (id, email) VALUES (999, 'test@example.com');
INSERT INTO orders (id, user_id, total) VALUES (9999, 999, 100.00);
-- Probar CASCADE DELETE
DELETE FROM users WHERE id = 999;
-- Verificar que la orden también fue eliminada
SELECT * FROM orders WHERE id = 9999;
-- Esperado: 0 filas
ROLLBACK;
3. Integridad de Dominio
La integridad de dominio asegura que los valores de columna se adhieran a restricciones definidas.
-- Prueba: Verificar restricciones NOT NULL
BEGIN;
INSERT INTO users (id, email, created_at)
VALUES (1, NULL, NOW());
-- Esperado: Error (si email es NOT NULL)
ROLLBACK;
-- Prueba: Verificar restricciones CHECK
BEGIN;
INSERT INTO products (id, name, price)
VALUES (1, 'Test Product', -10.00);
-- Esperado: Error (si existe restricción CHECK: price >= 0)
ROLLBACK;
-- Encontrar registros con patrones de datos inválidos
SELECT * FROM users WHERE email NOT LIKE '%@%';
SELECT * FROM orders WHERE total < 0;
4. Integridad Definida por el Usuario
La integridad definida por el usuario hace cumplir reglas de negocio específicas de la aplicación.
-- Prueba: Los totales de órdenes coinciden con la suma de líneas de artículos
SELECT
o.id AS order_id,
o.total AS order_total,
SUM(oi.quantity * oi.unit_price) AS calculated_total,
ABS(o.total - SUM(oi.quantity * oi.unit_price)) AS difference
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
GROUP BY o.id, o.total
HAVING ABS(o.total - SUM(oi.quantity * oi.unit_price)) > 0.01;
-- Esperado: 0 filas (dentro de tolerancia de redondeo)
-- Prueba: Los balances de cuenta son correctos
SELECT
a.id,
a.balance AS recorded_balance,
COALESCE(SUM(t.amount), 0) AS calculated_balance,
a.balance - COALESCE(SUM(t.amount), 0) AS difference
FROM accounts a
LEFT JOIN transactions t ON a.id = t.account_id
GROUP BY a.id, a.balance
HAVING ABS(a.balance - COALESCE(SUM(t.amount), 0)) > 0.01;
-- Esperado: 0 filas
Pruebas de Consistencia en Sistemas Distribuidos
Pruebas de Consistencia Eventual
import time
def test_eventual_consistency(db_writer, db_reader):
# Escribir a master
user_id = db_writer.create_user(email="test@example.com")
# Reintentar con exponential backoff
for attempt in range(5):
user = db_reader.get_user(user_id)
if user is None:
time.sleep(2 ** attempt)
else:
break
# Después de reintentos, debería ser consistente
assert user is not None
assert user.email == "test@example.com"
Performance Tuning y Testing
Identificación de Consultas Lentas
PostgreSQL:
-- Ver consultas lentas de pg_stat_statements
SELECT
query,
calls,
total_exec_time,
mean_exec_time,
max_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
-- Identificar consultas con alta varianza
SELECT
query,
calls,
mean_exec_time,
stddev_exec_time,
(stddev_exec_time / mean_exec_time) AS coefficient_of_variation
FROM pg_stat_statements
WHERE calls > 100
ORDER BY (stddev_exec_time / mean_exec_time) DESC;
MySQL:
-- Habilitar slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- Ver resumen de consultas lentas
SELECT
query,
exec_count,
avg_latency,
max_latency
FROM sys.statement_analysis
ORDER BY avg_latency DESC
LIMIT 20;
Optimización de Índices
Identificar índices faltantes:
-- PostgreSQL: Encontrar escaneos secuenciales en tablas grandes
SELECT
schemaname,
tablename,
seq_scan,
seq_tup_read,
idx_scan
FROM pg_stat_user_tables
WHERE seq_scan > 0
AND seq_tup_read / seq_scan > 10000
ORDER BY seq_tup_read DESC;
Probar efectividad del índice:
-- Probar plan EXPLAIN antes de agregar índice
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user@example.com';
-- Agregar índice
CREATE INDEX idx_users_email ON users(email);
-- Probar plan EXPLAIN después de agregar índice
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user@example.com';
Técnicas de Optimización de Consultas
**Evitar SELECT ***
-- Malo: Obtiene todas las columnas
SELECT * FROM users WHERE id = 123;
-- Bueno: Obtener solo columnas necesarias
SELECT id, email, name FROM users WHERE id = 123;
Evitar consultas N+1
# Malo: Problema de consulta N+1
users = db.query("SELECT * FROM users")
for user in users:
orders = db.query(f"SELECT * FROM orders WHERE user_id = {user.id}")
# Bueno: Usar JOIN
result = db.query("""
SELECT u.id, u.name, o.id as order_id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
""")
Migration Testing
Las migraciones de bases de datos están entre las operaciones más riesgosas en desarrollo de software.
Estrategia de Pruebas de Migración
1. Probar en no-producción primero (¡siempre!) 2. Usar herramientas de migración (Flyway, Liquibase, Alembic) 3. Hacer migraciones reversibles 4. Probar con volúmenes de datos similares a producción 5. Medir duración de migración
Pruebas de Migraciones de Esquema
-- Migración: agregar columna
-- UP
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT FALSE NOT NULL;
CREATE INDEX idx_users_email_verified ON users(email_verified);
-- DOWN (rollback)
DROP INDEX idx_users_email_verified;
ALTER TABLE users DROP COLUMN email_verified;
Checklist de pruebas:
- ✓ La migración ejecuta sin errores
- ✓ El valor predeterminado se aplica correctamente
- ✓ La restricción NOT NULL no rompe datos existentes
- ✓ El índice se crea exitosamente
- ✓ El script de rollback funciona correctamente
Pruebas de Migraciones de Datos
-- Migración: Dividir columna name en first_name y last_name
ALTER TABLE users ADD COLUMN first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN last_name VARCHAR(100);
UPDATE users
SET
first_name = SPLIT_PART(name, ' ', 1),
last_name = SPLIT_PART(name, ' ', 2)
WHERE name IS NOT NULL;
Pruebas de Migraciones a Gran Escala
Procesamiento por lotes:
-- En lugar de UPDATE único en millones de filas:
DO $$
DECLARE
batch_size INT := 10000;
rows_updated INT;
BEGIN
LOOP
UPDATE users
SET status = 'active'
WHERE id IN (
SELECT id FROM users
WHERE status IS NULL
LIMIT batch_size
);
GET DIAGNOSTICS rows_updated = ROW_COUNT;
EXIT WHEN rows_updated = 0;
COMMIT;
RAISE NOTICE 'Updated % rows', rows_updated;
PERFORM pg_sleep(0.1);
END LOOP;
END $$;
Pruebas de Bases de Datos NoSQL
Pruebas de MongoDB
// Definir reglas de validación de esquema
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["email", "createdAt"],
properties: {
email: {
bsonType: "string",
pattern: "^.+@.+$"
},
age: {
bsonType: "int",
minimum: 0,
maximum: 150
}
}
}
}
});
// Probar validación
db.users.insertOne({
email: "user@example.com",
age: 25,
createdAt: new Date()
});
Pruebas de Cassandra
from cassandra.cluster import Cluster
from cassandra import ConsistencyLevel
def test_consistency_levels():
# Escribir con QUORUM
insert_stmt = session.prepare("""
INSERT INTO users (id, email, name)
VALUES (?, ?, ?)
""")
insert_stmt.consistency_level = ConsistencyLevel.QUORUM
session.execute(insert_stmt, (uuid.uuid4(), 'test@example.com', 'Test'))
Pruebas de Redis
import redis
import time
def test_cache_expiration():
r = redis.Redis(host='localhost', port=6379)
# Establecer valor con TTL
r.setex('user:123:profile', 10, '{"name": "John"}')
# Inmediatamente disponible
assert r.get('user:123:profile') is not None
# Expirado después de 11 segundos
time.sleep(11)
assert r.get('user:123:profile') is None
Conclusión
Las pruebas de bases de datos son una disciplina crítica que requiere profundidad técnica y rigor sistemático.
Conclusiones clave:
- La integridad de datos es primordial - Usar restricciones, validar relaciones
- El rendimiento escala exponencialmente - Optimizar temprano, monitorear continuamente
- Las migraciones son de alto riesgo - Probar exhaustivamente, hacer reversibles
- NoSQL (como se discute en SQL Injection and XSS: Finding Vulnerabilities) requiere estrategias diferentes - Entender modelos de consistencia
- Automatizar pruebas de bases de datos - Integración en CI/CD detecta regresiones
- Monitorear bases de datos de producción - Usar logs de consultas lentas, rastrear métricas
Invierte en prácticas robustas de pruebas de bases de datos, y prevendrás corrupción de datos, evitarás crisis de rendimiento y entregarás sistemas confiables en los que los usuarios pueden confiar.