Las migraciones de bases de datos son operaciones críticas que pueden hacer o romper los despliegues en producción. Como se discute en API Testing Architecture: From Monoliths to Microservices, las pruebas adecuadas de bases de datos son esenciales para mantener la confiabilidad del sistema. Esta guía completa cubre estrategias de pruebas para migraciones de bases de datos usando Flyway y Liquibase, asegurando cambios de esquema seguros y despliegues sin tiempo de inactividad.
Comprendiendo los Desafíos de las Migraciones de Base de Datos
Las migraciones de bases de datos presentan desafíos únicos de prueba:
- Irreversibilidad: Muchos cambios de esquema no pueden deshacerse fácilmente
- Preservación de datos: Los datos existentes deben permanecer intactos y accesibles
- Requisitos de cero inactividad: Los sistemas de producción a menudo no pueden permitirse ventanas de mantenimiento
- Compatibilidad de versiones: El código de aplicación debe funcionar con múltiples versiones de esquema
- Impacto en el rendimiento: Las migraciones grandes pueden bloquear tablas y degradar el rendimiento (aprenda más sobre pruebas de rendimiento en Pruebas de Rendimiento de Bases de Datos)
Configuración y Setup de Flyway
Flyway usa migraciones basadas en SQL con control de versiones:
# Instalar Flyway CLI
brew install flyway
# Inicializar proyecto Flyway
flyway init
# Crear estructura de directorio de migración
mkdir -p db/migration
Configuración de Flyway (flyway.conf):
flyway.url=jdbc:postgresql://localhost:5432/mydb
flyway.user=dbuser
flyway.password=dbpassword
flyway.schemas=public
flyway.locations=filesystem:db/migration
flyway.validateOnMigrate=true
flyway.outOfOrder=false
flyway.baselineOnMigrate=true
Ejemplo de Migración (V1__create_users_table.sql):
-- V1__create_users_table.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
Pruebas de Migración con Flyway:
// flyway-test.js
const { Client } = require('pg');
const { execSync } = require('child_process');
describe('Migraciones Flyway', () => {
let client;
beforeAll(async () => {
// Limpiar base de datos
execSync('flyway clean -configFiles=flyway-test.conf');
client = new Client({
host: 'localhost',
port: 5432,
database: 'test_db',
user: 'test_user',
password: 'test_password'
});
await client.connect();
});
test('debe aplicar migraciones exitosamente', () => {
const result = execSync('flyway migrate -configFiles=flyway-test.conf');
expect(result.toString()).toContain('Successfully applied');
});
test('debe crear tabla users con esquema correcto', async () => {
const result = await client.query(`
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'users'
ORDER BY ordinal_position
`);
const columns = result.rows;
expect(columns).toContainEqual({
column_name: 'id',
data_type: 'integer',
is_nullable: 'NO'
});
expect(columns).toContainEqual({
column_name: 'email',
data_type: 'character varying',
is_nullable: 'NO'
});
});
test('debe tener índices correctos', async () => {
const result = await client.query(`
SELECT indexname
FROM pg_indexes
WHERE tablename = 'users'
`);
const indexNames = result.rows.map(r => r.indexname);
expect(indexNames).toContain('idx_users_email');
expect(indexNames).toContain('idx_users_username');
});
afterAll(async () => {
await client.end();
});
});
Configuración y Setup de Liquibase
Liquibase usa changesets XML, YAML, JSON o SQL:
<!-- liquibase.properties -->
changeLogFile=db/changelog/db.changelog-master.xml
url=jdbc:postgresql://localhost:5432/mydb
username=dbuser
password=dbpassword
driver=org.postgresql.Driver
Ejemplo de Changelog (db.changelog-master.xml):
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.0.xsd">
<changeSet id="1" author="developer">
<createTable tableName="products">
<column name="id" type="SERIAL">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="price" type="DECIMAL(10,2)">
<constraints nullable="false"/>
</column>
<column name="stock" type="INTEGER" defaultValue="0"/>
</createTable>
</changeSet>
<changeSet id="2" author="developer">
<createIndex tableName="products" indexName="idx_products_name">
<column name="name"/>
</createIndex>
</changeSet>
<changeSet id="3" author="developer">
<addColumn tableName="products">
<column name="category" type="VARCHAR(100)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
Pruebas de Liquibase:
// liquibase-test.js
const { execSync } = require('child_process');
const { Client } = require('pg');
describe('Migraciones Liquibase', () => {
let client;
beforeAll(async () => {
client = new Client({
connectionString: 'postgresql://test_user:test_password@localhost:5432/test_db'
});
await client.connect();
// Limpiar base de datos
execSync('liquibase dropAll --defaultsFile=liquibase-test.properties');
});
test('debe aplicar todos los changesets', () => {
const result = execSync('liquibase update --defaultsFile=liquibase-test.properties');
expect(result.toString()).toContain('successfully');
});
test('debe rastrear ejecución de changesets', async () => {
const result = await client.query('SELECT * FROM databasechangelog');
expect(result.rows.length).toBeGreaterThan(0);
expect(result.rows[0]).toHaveProperty('id');
expect(result.rows[0]).toHaveProperty('author');
expect(result.rows[0]).toHaveProperty('filename');
});
test('debe crear tabla products con todas las columnas', async () => {
const result = await client.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'products'
`);
const columnNames = result.rows.map(r => r.column_name);
expect(columnNames).toContain('id');
expect(columnNames).toContain('name');
expect(columnNames).toContain('price');
expect(columnNames).toContain('stock');
expect(columnNames).toContain('category');
});
afterAll(async () => {
await client.end();
});
});
Estrategias de Pruebas de Rollback
Probar capacidades de rollback es crucial para la seguridad en producción:
Pruebas de Rollback de Flyway
Flyway no soporta rollbacks automáticos, pero puede crear migraciones de deshacer:
-- V2__add_status_column.sql
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active';
-- U2__remove_status_column.sql (migración de deshacer)
ALTER TABLE users DROP COLUMN status;
Probando Rollback:
describe('Rollback Flyway', () => {
test('debe hacer rollback de migración exitosamente', async () => {
// Aplicar migración
execSync('flyway migrate -configFiles=flyway-test.conf');
// Verificar que existe columna
let result = await client.query(`
SELECT column_name FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'status'
`);
expect(result.rows.length).toBe(1);
// Aplicar migración de deshacer
execSync('flyway undo -configFiles=flyway-test.conf');
// Verificar columna removida
result = await client.query(`
SELECT column_name FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'status'
`);
expect(result.rows.length).toBe(0);
});
});
Pruebas de Rollback de Liquibase
Liquibase tiene soporte incorporado de rollback:
<changeSet id="4" author="developer">
<addColumn tableName="products">
<column name="description" type="TEXT"/>
</addColumn>
<rollback>
<dropColumn tableName="products" columnName="description"/>
</rollback>
</changeSet>
Probando Rollback de Liquibase:
describe('Rollback Liquibase', () => {
test('debe hacer rollback a versión anterior', async () => {
// Obtener versión actual
const versionBefore = await client.query(
'SELECT COUNT(*) FROM databasechangelog'
);
// Aplicar nuevo changeset
execSync('liquibase update --defaultsFile=liquibase-test.properties');
// Hacer rollback de un changeset
execSync('liquibase rollbackCount 1 --defaultsFile=liquibase-test.properties');
// Verificar rollback
const versionAfter = await client.query(
'SELECT COUNT(*) FROM databasechangelog'
);
expect(versionAfter.rows[0].count).toBe(versionBefore.rows[0].count);
});
test('debe hacer rollback por etiqueta', async () => {
// Etiquetar estado actual
execSync('liquibase tag version-1.0 --defaultsFile=liquibase-test.properties');
// Aplicar más cambios
execSync('liquibase update --defaultsFile=liquibase-test.properties');
// Rollback a etiqueta
execSync('liquibase rollback version-1.0 --defaultsFile=liquibase-test.properties');
// Verificar que el estado coincide con etiqueta
const result = await client.query(
"SELECT tag FROM databasechangelog WHERE tag = 'version-1.0'"
);
expect(result.rows.length).toBeGreaterThan(0);
});
});
Validación de Integridad de Datos
Asegurar que las migraciones preservan y transforman correctamente los datos. Para obtener información más profunda sobre estrategias de pruebas de bases de datos, consulte nuestro artículo Pruebas de Bases de Datos en Profundidad:
// data-integrity-test.js
describe('Integridad de Datos Durante Migración', () => {
beforeEach(async () => {
// Insertar datos de prueba
await client.query(`
INSERT INTO users (email, username)
VALUES
('user1@example.com', 'user1'),
('user2@example.com', 'user2'),
('user3@example.com', 'user3')
`);
});
test('debe preservar datos existentes durante migración', async () => {
const countBefore = await client.query('SELECT COUNT(*) FROM users');
// Aplicar migración que añade columna
execSync('flyway migrate -configFiles=flyway-test.conf');
const countAfter = await client.query('SELECT COUNT(*) FROM users');
expect(countAfter.rows[0].count).toBe(countBefore.rows[0].count);
});
test('debe transformar datos correctamente', async () => {
// Migración: V3__split_username.sql
// Divide 'username' en 'first_name' y 'last_name'
await client.query(`
UPDATE users SET username = 'John Doe' WHERE email = 'user1@example.com'
`);
execSync('flyway migrate -target=3 -configFiles=flyway-test.conf');
const result = await client.query(`
SELECT first_name, last_name
FROM users
WHERE email = 'user1@example.com'
`);
expect(result.rows[0].first_name).toBe('John');
expect(result.rows[0].last_name).toBe('Doe');
});
test('debe manejar valores nulos correctamente', async () => {
await client.query(`
INSERT INTO users (email, username) VALUES ('null@example.com', NULL)
`);
execSync('flyway migrate -configFiles=flyway-test.conf');
const result = await client.query(`
SELECT * FROM users WHERE email = 'null@example.com'
`);
expect(result.rows.length).toBe(1);
expect(result.rows[0].username).toBeNull();
});
afterEach(async () => {
await client.query('TRUNCATE users CASCADE');
});
});
Probando Migraciones en CI/CD
Integrar pruebas de migración en su pipeline CI/CD. Para una guía completa sobre cómo configurar CI/CD para pruebas, consulte nuestra guía sobre Pipeline CI/CD para Testers:
# .github/workflows/db-migration-test.yml
name: Pruebas de Migración de Base de Datos
on:
pull_request:
paths:
- 'db/migration/**'
- 'db/changelog/**'
jobs:
test-migrations:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Configurar Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Instalar Flyway
run: |
wget -qO- https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/9.16.0/flyway-commandline-9.16.0-linux-x64.tar.gz | tar xvz
sudo ln -s `pwd`/flyway-9.16.0/flyway /usr/local/bin
- name: Ejecutar pruebas de migración
run: |
npm install
npm run test:migrations
env:
DB_HOST: localhost
DB_PORT: 5432
DB_NAME: test_db
DB_USER: test_user
DB_PASSWORD: test_password
- name: Probar rollback
run: npm run test:rollback
- name: Validar checksums de migración
run: flyway validate -configFiles=flyway-test.conf
Patrones de Despliegue sin Tiempo de Inactividad
Patrón Expandir-Contraer
// Fase 1: Expandir - Añadir nueva columna (compatible hacia atrás)
// V4__add_email_verified_column.sql
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT FALSE;
// Fase 2: Migrar datos
// V5__migrate_email_verification.sql
UPDATE users
SET email_verified = (
SELECT EXISTS(
SELECT 1 FROM email_verifications
WHERE email_verifications.user_id = users.id
)
);
// Fase 3: Contraer - Eliminar tabla antigua (después del despliegue)
// V6__remove_email_verifications_table.sql
DROP TABLE email_verifications;
Probando Migración sin Tiempo de Inactividad:
describe('Migración sin Tiempo de Inactividad', () => {
test('aplicación funciona durante fase de expansión', async () => {
// Aplicar migración de expansión
execSync('flyway migrate -target=4 -configFiles=flyway-test.conf');
// Simular código de aplicación antiguo
await client.query(`
INSERT INTO users (email, username)
VALUES ('test@example.com', 'testuser')
`);
// Verificar que funciona esquema antiguo y nuevo
const result = await client.query('SELECT * FROM users WHERE email = $1', [
'test@example.com'
]);
expect(result.rows[0]).toHaveProperty('email');
expect(result.rows[0]).toHaveProperty('email_verified');
});
test('migración de datos preserva integridad', async () => {
// Insertar datos de prueba
await client.query(`
INSERT INTO users (email, username) VALUES ('user@example.com', 'user')
`);
await client.query(`
INSERT INTO email_verifications (user_id) VALUES (1)
`);
// Aplicar migración de datos
execSync('flyway migrate -target=5 -configFiles=flyway-test.conf');
const result = await client.query('SELECT email_verified FROM users WHERE id = 1');
expect(result.rows[0].email_verified).toBe(true);
});
});
Mejores Prácticas de Pruebas de Migración
Lista de Verificación de Pruebas
- Probar migraciones en base de datos limpia
- Probar migraciones en base de datos con datos existentes
- Verificar que todos los índices se crean correctamente
- Probar restricciones de claves foráneas
- Validar valores predeterminados y restricciones
- Probar procedimientos de rollback
- Verificar integridad de datos después de migración
- Probar rendimiento de migraciones de datos grandes
- Verificar problemas de bloqueo en tablas grandes
- Probar migraciones en múltiples versiones de base de datos
- Validar checksums de migración
- Probar escenarios de migración concurrente
Comparación de Pruebas de Migración
Aspecto | Flyway | Liquibase |
---|---|---|
Soporte Rollback | Migraciones de deshacer manuales | Rollback incorporado |
Formato | Archivos SQL | XML, YAML, JSON, SQL |
Curva de Aprendizaje | Baja (SQL familiar) | Media (sintaxis XML) |
Control de Versiones | Versionado basado en archivos | IDs de changeset |
Lógica Condicional | Limitada | Extensa (precondiciones) |
Soporte de BD | Amplio | Amplio |
Generación Diff | Solo comercial | Incorporado |
Conclusión
Las pruebas efectivas de migración de bases de datos requieren estrategias completas que cubran validación de esquema, integridad de datos, procedimientos de rollback y patrones de despliegue sin tiempo de inactividad. Al implementar pruebas exhaustivas con Flyway o Liquibase, integrar pruebas en pipelines CI/CD y seguir las mejores prácticas, puede asegurar cambios de base de datos seguros y confiables.
Conclusiones clave:
- Siempre probar migraciones en base de datos limpia y con datos existentes
- Implementar y probar procedimientos de rollback para cada migración
- Usar patrón expandir-contraer para despliegues sin tiempo de inactividad
- Validar integridad de datos antes y después de migraciones
- Probar en múltiples versiones de base de datos
- Automatizar pruebas de migración en pipelines CI/CD
Las pruebas robustas de migración generan confianza en los cambios de base de datos y previenen incidentes costosos en producción.