Database Testing Strategies for Developers


Database Testing Strategies

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.