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:

  1. La integridad de datos es primordial - Usar restricciones, validar relaciones
  2. El rendimiento escala exponencialmente - Optimizar temprano, monitorear continuamente
  3. Las migraciones son de alto riesgo - Probar exhaustivamente, hacer reversibles
  4. NoSQL (como se discute en SQL Injection and XSS: Finding Vulnerabilities) requiere estrategias diferentes - Entender modelos de consistencia
  5. Automatizar pruebas de bases de datos - Integración en CI/CD detecta regresiones
  6. 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.