home / skills / microck / ordinary-claude-skills / database-testing

database-testing skill

/skills_all/database-testing

This skill helps you validate database schemas, ensure data integrity, test migrations, and measure query performance to prevent data issues.

npx playbooks add skill microck/ordinary-claude-skills --skill database-testing

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
6.3 KB
---
name: database-testing
description: Database schema validation, data integrity testing, migration testing, transaction isolation, and query performance. Use when testing data persistence, ensuring referential integrity, or validating database migrations.
---

# Database Testing

## Core Principle

**Data is your most valuable asset. Database bugs cause data loss/corruption.**

Database testing ensures schema correctness, data integrity, transaction safety, and query performance. Critical for preventing catastrophic data issues.

## Schema Testing

**Validate database structure:**
```sql
-- Test schema exists
SELECT table_name FROM information_schema.tables 
WHERE table_schema = 'public' AND table_name = 'users';

-- Test column types
SELECT column_name, data_type 
FROM information_schema.columns 
WHERE table_name = 'users';

-- Test constraints
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_name = 'users';
```

**Test with code:**
```javascript
test('users table has correct schema', async () => {
  const schema = await db.raw(`
    SELECT column_name, data_type, is_nullable
    FROM information_schema.columns
    WHERE table_name = 'users'
  `);

  expect(schema).toContainEqual({
    column_name: 'id',
    data_type: 'integer',
    is_nullable: 'NO'
  });

  expect(schema).toContainEqual({
    column_name: 'email',
    data_type: 'character varying',
    is_nullable: 'NO'
  });
});
```

## Data Integrity Testing

**Test constraints:**
```javascript
test('email must be unique', async () => {
  await db.users.create({ email: '[email protected]' });

  // Duplicate should fail
  await expect(
    db.users.create({ email: '[email protected]' })
  ).rejects.toThrow('unique constraint violation');
});

test('foreign key prevents orphaned records', async () => {
  const user = await db.users.create({ email: '[email protected]' });
  await db.orders.create({ userId: user.id, total: 100 });

  // Cannot delete user with orders
  await expect(
    db.users.delete({ id: user.id })
  ).rejects.toThrow('foreign key constraint');
});

test('check constraint validates data', async () => {
  // Age must be ≥ 18
  await expect(
    db.users.create({ email: '[email protected]', age: 17 })
  ).rejects.toThrow('check constraint violation');
});
```

## Migration Testing

**Test database migrations:**
```javascript
import { migrate, rollback } from './migrations';

test('migration adds users table', async () => {
  // Start fresh
  await rollback();

  // Run migration
  await migrate();

  // Verify table exists
  const tables = await db.raw(`
    SELECT table_name FROM information_schema.tables
    WHERE table_schema = 'public'
  `);

  expect(tables.map(t => t.table_name)).toContain('users');
});

test('migration is reversible', async () => {
  await migrate();
  await rollback();

  // Table should be gone
  const tables = await db.raw(`
    SELECT table_name FROM information_schema.tables
    WHERE table_schema = 'public'
  `);

  expect(tables.map(t => t.table_name)).not.toContain('users');
});

test('migration preserves existing data', async () => {
  // Create data before migration
  await db.users.create({ email: '[email protected]' });

  // Run migration that adds 'age' column
  await migrate('add-age-column');

  // Data should still exist
  const user = await db.users.findOne({ email: '[email protected]' });
  expect(user).toBeDefined();
  expect(user.age).toBeNull(); // New column, null default
});
```

## Transaction Isolation Testing

**Test ACID properties:**
```javascript
test('transaction rolls back on error', async () => {
  const initialCount = await db.users.count();

  try {
    await db.transaction(async (trx) => {
      await trx('users').insert({ email: '[email protected]' });
      await trx('users').insert({ email: '[email protected]' });

      // Force error
      throw new Error('Rollback test');
    });
  } catch (error) {
    // Expected
  }

  // No users should be inserted
  const finalCount = await db.users.count();
  expect(finalCount).toBe(initialCount);
});

test('concurrent transactions are isolated', async () => {
  const user = await db.users.create({ email: '[email protected]', balance: 100 });

  // Two concurrent withdrawals
  const withdraw1 = db.transaction(async (trx) => {
    const current = await trx('users').where({ id: user.id }).first();
    await sleep(100); // Simulate delay
    await trx('users').where({ id: user.id }).update({ 
      balance: current.balance - 50 
    });
  });

  const withdraw2 = db.transaction(async (trx) => {
    const current = await trx('users').where({ id: user.id }).first();
    await sleep(100);
    await trx('users').where({ id: user.id }).update({ 
      balance: current.balance - 50 
    });
  });

  await Promise.all([withdraw1, withdraw2]);

  // With proper isolation, balance should be 0, not 50
  const final = await db.users.findOne({ id: user.id });
  expect(final.balance).toBe(0); // Not 50!
});
```

## Query Performance Testing

**Test slow queries:**
```javascript
test('user lookup by email is fast', async () => {
  // Seed 10,000 users
  await seedUsers(10000);

  const start = Date.now();
  await db.users.findOne({ email: '[email protected]' });
  const duration = Date.now() - start;

  // Should use index on email
  expect(duration).toBeLessThan(10); // < 10ms
});

test('EXPLAIN shows index usage', async () => {
  const explain = await db.raw(`
    EXPLAIN SELECT * FROM users WHERE email = '[email protected]'
  `);

  // Should show index scan, not sequential scan
  const plan = explain.rows[0]['QUERY PLAN'];
  expect(plan).toContain('Index Scan');
  expect(plan).not.toContain('Seq Scan');
});
```

## Related Skills

- [test-data-management](../test-data-management/) - Generate test data for DB
- [performance-testing](../performance-testing/) - DB performance testing
- [test-automation-strategy](../test-automation-strategy/) - Automate DB tests

## Remember

**Database bugs are catastrophic.**

- Data loss is unrecoverable
- Corruption spreads silently
- Performance issues compound
- Migrations must be reversible

**Test migrations before production:**
- Forward migration works
- Backward rollback works
- Data preserved/migrated correctly
- Performance acceptable

**With Agents:** `qe-test-data-architect` generates realistic test data with referential integrity. `qe-test-executor` runs DB migration tests automatically in CI/CD.

Overview

This skill provides a practical toolkit for database testing focused on schema validation, data integrity, migration safety, transaction isolation, and query performance. It helps teams detect schema drift, prevent data corruption, validate migrations, and measure query behavior before production. The examples and patterns are language-agnostic but include runnable snippets and test ideas in common test frameworks.

How this skill works

The skill inspects database metadata (information_schema), enforces constraints through test code, and runs migration and rollback scenarios to verify reversibility and data preservation. It runs transaction-based tests to confirm ACID behavior and concurrent isolation, and it measures query latency and EXPLAIN plans to ensure index usage and acceptable performance. Tests are designed to run in CI against disposable test instances or ephemeral databases.

When to use it

  • Before releasing schema changes or migrations to production
  • When adding or modifying constraints (unique, foreign key, check)
  • To validate transaction behavior and concurrent operations
  • When diagnosing slow queries or ensuring index coverage
  • As part of CI/CD to catch regressions in data integrity

Best practices

  • Run tests against isolated, reproducible test databases seeded with realistic data
  • Automate migration forward and rollback tests to ensure reversibility
  • Assert schema by querying information_schema rather than relying on ORM models alone
  • Use transactions and cleanup hooks so tests are idempotent and leave no residue
  • Measure query durations under realistic load and check EXPLAIN plans for index scans

Example use cases

  • Validate that a new migration adds an 'age' column without losing existing user records
  • Assert that email uniqueness is enforced and duplicate inserts raise constraint errors
  • Confirm that deleting a parent fails while child records exist due to foreign keys
  • Simulate concurrent withdrawals to verify transaction isolation prevents lost updates
  • Seed 10k users to verify a lookup by email uses an index and meets latency targets

FAQ

Can I run these tests in CI without a full production database?

Yes. Use ephemeral databases or containerized instances populated with representative test data so tests stay fast and isolated.

How do I test migration reversibility safely?

Run migrations against a fresh test database, verify schema and data, then run rollback and assert that schema changes are undone and critical data is preserved or migrated as intended.