Database Testing Strategies

Database Testing Strategies for Developers

Database testing is often the weakest part of test suites. Effective strategies catch issues before production.

Unit Tests with In-Memory DB

In-memory databases provide fast feedback during development:

@pytest.fixture

def repo():

conn = sqlite3.connect(':memory:')

conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)")

return UserRepository(conn)

def test_find_by_email(repo):

repo.create("alice@example.com", "Alice")

user = repo.find_by_email("alice@example.com")

assert user is not None

Integration Tests with Testcontainers

Testcontainers spins up disposable database containers:

@pytest.fixture(scope="module")

def postgres():

with PostgresContainer("postgres:16") as pg:

yield pg

def test_order_total(repo):

repo.create("2026-01-01", 100.00)

total = repo.total_sales_between("2026-01-01", "2026-01-31")

assert total == 100.00

Migration Testing

Test that migrations are backward-compatible and rollbacks work:

def test_rollback():

apply_migration("V1__initial.sql")

apply_migration("V2__add_column.sql")

rollback_migration("V2__add_column.sql")

columns = get_table_columns("users")

assert "new_column" not in columns

Data Fixtures

Use factory patterns for test data:

class UserFactory:

email = factory.Sequence(lambda n: f"user{n}@example.com")

name = factory.Faker('name')

Conclusion

Use in-memory databases for fast unit tests. Use Testcontainers for integration tests against real databases. Test migrations for backward compatibility. Use factory patterns for data fixtures. The most valuable test is one that uses a real database.