Вызов Тестирования Баз Данных в DevOps
Изменения в базах данных часто являются самой рискованной частью развертывания программного обеспечения. Традиционные подходы к управлению базами данных - ручные изменения схемы, несогласованные окружения и отсутствие контроля версий - создают узкие места, которые замедляют CI/CD пайплайны и увеличивают риск развертывания. Современные практики DevOps требуют, чтобы изменения баз данных тестировались, версионировались и автоматизировались с той же тщательностью, что и код приложения.
Эта статья исследует комплексные стратегии Database DevOps для автоматизации тестирования, охватывая инструменты миграции (Flyway, Liquibase), валидацию схем, управление тестовыми данными, тестирование rollback и интеграцию CI/CD.
Тестирование Миграций Баз Данных с Flyway
Конфигурация и Тестирование Flyway
-- V1__create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(100) UNIQUE 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);
# tests/test_flyway_migrations.py
class FlywayMigrationTester:
def test_migration_execution(self):
"""Проверить что миграции выполняются успешно"""
self.flyway.clean()
result = self.flyway.migrate()
assert result.success
assert result.migrations_executed > 0
def test_migration_idempotency(self):
"""Проверить что миграции идемпотентны"""
result1 = self.flyway.migrate()
result2 = self.flyway.migrate()
# Второй запуск не должен выполнять миграции
assert result2.migrations_executed == 0
def test_schema_validation(self):
"""Валидировать схему после миграций"""
self.flyway.migrate()
conn = psycopg2.connect(**self.db_config)
cursor = conn.cursor()
# Проверить что таблицы существуют
cursor.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
""")
tables = [row[0] for row in cursor.fetchall()]
assert 'users' in tables
# Проверить колонки
cursor.execute("""
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'users'
""")
columns = {row[0]: {'type': row[1], 'nullable': row[2]} for row in cursor.fetchall()}
assert 'id' in columns
assert columns['email']['nullable'] == 'NO'
conn.close()
Интеграция CI/CD с Flyway
# .gitlab-ci.yml
database_test:
stage: test
image: postgres:15
services:
- postgres:15
variables:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
script:
- pytest tests/test_flyway_migrations.py -v
migrate_staging:
stage: migrate
image: flyway/flyway:latest
script:
- flyway migrate
-url=jdbc:postgresql://$DB_HOST:5432/$DB_NAME
-user=$DB_USER
-password=$DB_PASSWORD
-locations=filesystem:migrations
environment:
name: staging
only:
- main
Интеграция и Тестирование Liquibase
Changesets Liquibase
<!-- changesets/001-create-products-table.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">
<changeSet id="001" author="qa-team">
<createTable tableName="products">
<column name="id" type="BIGINT" autoIncrement="true">
<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="INT" defaultValue="0">
<constraints nullable="false"/>
</column>
</createTable>
<rollback>
<dropTable tableName="products"/>
</rollback>
</changeSet>
</databaseChangeLog>
Фреймворк Тестирования Liquibase
# tests/test_liquibase_migrations.py
class LiquibaseTester:
def run_liquibase_command(self, command: str):
"""Выполнить команду Liquibase"""
cmd = [
"liquibase",
f"--url={self.jdbc_url}",
f"--username={self.db_config['user']}",
f"--password={self.db_config['password']}",
f"--changeLogFile={self.changelog_path}",
command
]
return subprocess.run(cmd, capture_output=True, text=True)
def test_changeset_validation(self):
"""Проверить что changesets валидны"""
result = self.run_liquibase_command("validate")
assert result.returncode == 0
def test_update_execution(self):
"""Проверить что update выполняется успешно"""
self.run_liquibase_command("dropAll")
result = self.run_liquibase_command("update")
assert result.returncode == 0
def test_rollback_functionality(self):
"""Проверить функциональность rollback"""
self.run_liquibase_command("update")
self.run_liquibase_command("tag version-1.0")
result = self.run_liquibase_command("rollback version-1.0")
assert result.returncode == 0
Детекция Schema Drift
# tests/test_schema_drift.py
class SchemaDriftDetector:
def get_actual_schema(self):
"""Получить актуальную схему базы данных"""
inspector = inspect(self.engine)
schema = {'tables': {}, 'indexes': {}}
for table_name in inspector.get_table_names():
columns = {}
for column in inspector.get_columns(table_name):
columns[column['name']] = {
'type': str(column['type']),
'nullable': column['nullable']
}
schema['tables'][table_name] = columns
indexes = []
for index in inspector.get_indexes(table_name):
indexes.append({
'name': index['name'],
'columns': index['column_names']
})
schema['indexes'][table_name] = indexes
return schema
def test_no_schema_drift(self):
"""Проверить что актуальная схема соответствует ожидаемой"""
actual = self.get_actual_schema()
expected = self.expected_schema
actual_tables = set(actual['tables'].keys())
expected_tables = set(expected['tables'].keys())
missing_tables = expected_tables - actual_tables
extra_tables = actual_tables - expected_tables
assert not missing_tables, f"Отсутствующие таблицы: {missing_tables}"
assert not extra_tables, f"Неожиданные таблицы: {extra_tables}"
Управление Тестовыми Данными
# tests/fixtures/test_data_seeder.py
class TestDataSeeder:
def seed_users(self, count: int = 100):
"""Заполнить тестовые данные пользователей"""
session = self.Session()
for _ in range(count):
user = {
'email': self.faker.email(),
'username': self.faker.user_name(),
'first_name': self.faker.first_name(),
'last_name': self.faker.last_name()
}
session.execute(
"INSERT INTO users (email, username, first_name, last_name) "
"VALUES (:email, :username, :first_name, :last_name)",
user
)
session.commit()
session.close()
def clean_all_tables(self):
"""Очистить все тестовые данные"""
session = self.Session()
session.execute("SET session_replication_role = 'replica';")
result = session.execute("""
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
AND tablename NOT IN ('databasechangelog', 'flyway_schema_history')
""")
for row in result:
session.execute(f"TRUNCATE TABLE {row[0]} CASCADE")
session.execute("SET session_replication_role = 'origin';")
session.commit()
session.close()
@pytest.fixture(scope="function")
def seeded_database(database_url):
"""Pytest fixture для заполнения и очистки БД"""
seeder = TestDataSeeder(database_url)
seeder.seed_users(100)
seeder.seed_products(50)
yield seeder
seeder.clean_all_tables()
Workflow GitHub Actions
# .github/workflows/database-ci.yml
name: Database CI/CD
on:
pull_request:
paths:
- 'migrations/**'
push:
branches:
- main
jobs:
test-migrations:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install pytest psycopg2-binary sqlalchemy faker
- name: Install Flyway
run: |
wget -qO- https://download.red-gate.com/maven/release/com/redgate/flyway/flyway-commandline/10.0.0/flyway-commandline-10.0.0-linux-x64.tar.gz | tar -xz
sudo ln -s $(pwd)/flyway-10.0.0/flyway /usr/local/bin/flyway
- name: Run Flyway tests
run: |
pytest tests/test_flyway_migrations.py -v
- name: Run schema drift tests
run: |
pytest tests/test_schema_drift.py -v
Заключение
Database DevOps для автоматизации тестирования требует отношения к изменениям баз данных как к коду с комплексным тестированием, контролем версий и автоматизированными стратегиями развертывания. Внедряя тестирование миграций с Flyway и Liquibase, детекцию schema drift, автоматизированное заполнение тестовых данных и интеграцию CI/CD, команды могут достичь непрерывной доставки баз данных с уверенностью.
Ключ - установить автоматизированную валидацию для каждого изменения базы данных - от скриптов миграции до модификаций схемы - обеспечивая согласованность между окружениями и предотвращая проблемы в продакшене.