+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 221 of 365

๐Ÿ“˜ Testing Database Code: Test Databases

Master testing database code: test databases in Python with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
30 min read

Prerequisites

  • Basic understanding of programming concepts ๐Ÿ“
  • Python installation (3.8+) ๐Ÿ
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write clean, Pythonic code โœจ

๐ŸŽฏ Introduction

Welcome to this exciting tutorial on testing database code! ๐ŸŽ‰ Have you ever wondered how to test your database-driven applications without messing up production data? Or struggled with tests that fail randomly because of database state? Youโ€™re in the right place!

Testing database code is like having a sandbox ๐Ÿ–๏ธ where you can build and destroy castles without worrying about the real beach. Youโ€™ll discover how test databases give you the freedom to experiment, validate your code, and sleep peacefully knowing your tests wonโ€™t accidentally delete important data! ๐Ÿ˜ด

By the end of this tutorial, youโ€™ll be confidently setting up test databases, writing reliable database tests, and following best practices that professional developers use every day. Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Test Databases

๐Ÿค” What are Test Databases?

Test databases are like practice rooms for musicians ๐ŸŽธ. Just as musicians need a safe space to practice without disturbing others, developers need isolated databases to test their code without affecting real data.

In Python terms, a test database is a separate database instance used exclusively for running tests. This means you can:

  • โœจ Create, modify, and delete data freely
  • ๐Ÿš€ Run tests in parallel without conflicts
  • ๐Ÿ›ก๏ธ Protect production data from test operations
  • ๐Ÿ”„ Reset to a clean state for each test

๐Ÿ’ก Why Use Test Databases?

Hereโ€™s why developers love test databases:

  1. Isolation ๐Ÿ๏ธ: Each test runs in its own clean environment
  2. Reproducibility ๐Ÿ”„: Tests produce consistent results every time
  3. Safety ๐Ÿ›ก๏ธ: No risk of corrupting production data
  4. Speed โšก: Tests can run in parallel without interference
  5. Confidence ๐Ÿ’ช: Test real database operations, not mocks

Real-world example: Imagine testing an e-commerce system ๐Ÿ›’. With test databases, you can simulate thousands of orders, test edge cases like payment failures, and verify inventory updatesโ€”all without touching a single real customerโ€™s data!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Setting Up a Test Database with SQLite

Letโ€™s start with a simple example using SQLite (perfect for testing!):

import sqlite3
import pytest
from pathlib import Path

# ๐ŸŽจ Create a test database fixture
@pytest.fixture
def test_db():
    # ๐Ÿ‘‹ Create a temporary database
    db_path = Path("test_shop.db")
    connection = sqlite3.connect(db_path)
    
    # ๐Ÿ—๏ธ Set up schema
    cursor = connection.cursor()
    cursor.execute("""
        CREATE TABLE products (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            price REAL NOT NULL,
            stock INTEGER DEFAULT 0
        )
    """)
    connection.commit()
    
    # ๐ŸŽฏ Yield the connection for tests
    yield connection
    
    # ๐Ÿงน Clean up after test
    connection.close()
    db_path.unlink()  # Delete test database

# ๐ŸŽฎ Use the test database
def test_add_product(test_db):
    cursor = test_db.cursor()
    
    # โž• Add a product
    cursor.execute(
        "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
        ("Python Book ๐Ÿ“˜", 29.99, 10)
    )
    test_db.commit()
    
    # โœ… Verify it was added
    cursor.execute("SELECT * FROM products WHERE name LIKE '%Python%'")
    product = cursor.fetchone()
    
    assert product is not None
    assert product[1] == "Python Book ๐Ÿ“˜"
    assert product[2] == 29.99

๐Ÿ’ก Explanation: Notice how we create a fresh database for each test and clean it up afterward. This ensures tests donโ€™t interfere with each other!

๐ŸŽฏ Using PostgreSQL for Testing

For more complex applications, you might use PostgreSQL:

import psycopg2
import pytest
from contextlib import contextmanager

# ๐Ÿ—๏ธ Database configuration
TEST_DB_CONFIG = {
    'host': 'localhost',
    'port': 5432,
    'user': 'test_user',
    'password': 'test_pass',
    'database': 'test_shop_db'
}

# ๐ŸŽจ Context manager for test transactions
@contextmanager
def test_transaction():
    conn = psycopg2.connect(**TEST_DB_CONFIG)
    conn.autocommit = False
    
    try:
        yield conn
        conn.rollback()  # ๐Ÿ”„ Always rollback to keep DB clean!
    finally:
        conn.close()

# ๐Ÿงช Test with automatic rollback
def test_inventory_update():
    with test_transaction() as conn:
        cursor = conn.cursor()
        
        # ๐Ÿ“ฆ Insert test product
        cursor.execute("""
            INSERT INTO products (name, price, stock) 
            VALUES (%s, %s, %s) RETURNING id
        """, ("Gaming Mouse ๐Ÿ–ฑ๏ธ", 49.99, 50))
        
        product_id = cursor.fetchone()[0]
        
        # ๐Ÿ›’ Simulate a purchase
        cursor.execute("""
            UPDATE products 
            SET stock = stock - %s 
            WHERE id = %s AND stock >= %s
        """, (5, product_id, 5))
        
        # โœ… Verify stock was updated
        cursor.execute("SELECT stock FROM products WHERE id = %s", (product_id,))
        new_stock = cursor.fetchone()[0]
        
        assert new_stock == 45
        # ๐ŸŽ‰ Transaction will rollback, keeping DB clean!

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: E-Commerce Order System

Letโ€™s build a complete test suite for an order system:

import sqlite3
from datetime import datetime
from decimal import Decimal
import pytest

# ๐Ÿช Our e-commerce system
class OrderSystem:
    def __init__(self, db_connection):
        self.conn = db_connection
        self.cursor = db_connection.cursor()
    
    # ๐Ÿ›’ Create a new order
    def create_order(self, customer_id, items):
        # ๐Ÿ“ Create order record
        self.cursor.execute("""
            INSERT INTO orders (customer_id, total, status, created_at)
            VALUES (?, 0, 'pending', ?)
        """, (customer_id, datetime.now()))
        
        order_id = self.cursor.lastrowid
        total = Decimal('0')
        
        # ๐Ÿ“ฆ Add order items
        for item in items:
            product_id, quantity = item['product_id'], item['quantity']
            
            # ๐Ÿ” Get product details
            self.cursor.execute(
                "SELECT price, stock FROM products WHERE id = ?",
                (product_id,)
            )
            price, stock = self.cursor.fetchone()
            
            if stock < quantity:
                raise ValueError(f"Insufficient stock! ๐Ÿ˜ฑ Only {stock} available")
            
            # โž• Add to order
            self.cursor.execute("""
                INSERT INTO order_items (order_id, product_id, quantity, price)
                VALUES (?, ?, ?, ?)
            """, (order_id, product_id, quantity, price))
            
            # ๐Ÿ“‰ Update stock
            self.cursor.execute(
                "UPDATE products SET stock = stock - ? WHERE id = ?",
                (quantity, product_id)
            )
            
            total += Decimal(str(price)) * quantity
        
        # ๐Ÿ’ฐ Update order total
        self.cursor.execute(
            "UPDATE orders SET total = ? WHERE id = ?",
            (float(total), order_id)
        )
        
        self.conn.commit()
        return order_id

# ๐Ÿงช Test fixtures
@pytest.fixture
def test_database():
    # ๐ŸŽจ Create test database with schema
    conn = sqlite3.connect(':memory:')  # In-memory for speed! โšก
    cursor = conn.cursor()
    
    # ๐Ÿ“Š Create tables
    cursor.executescript("""
        CREATE TABLE products (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            price REAL NOT NULL,
            stock INTEGER DEFAULT 0
        );
        
        CREATE TABLE orders (
            id INTEGER PRIMARY KEY,
            customer_id INTEGER NOT NULL,
            total REAL DEFAULT 0,
            status TEXT DEFAULT 'pending',
            created_at TIMESTAMP
        );
        
        CREATE TABLE order_items (
            id INTEGER PRIMARY KEY,
            order_id INTEGER REFERENCES orders(id),
            product_id INTEGER REFERENCES products(id),
            quantity INTEGER NOT NULL,
            price REAL NOT NULL
        );
    """)
    
    # ๐ŸŽฎ Add test data
    test_products = [
        ("Gaming Keyboard โŒจ๏ธ", 89.99, 20),
        ("Wireless Mouse ๐Ÿ–ฑ๏ธ", 49.99, 15),
        ("USB-C Cable ๐Ÿ”Œ", 12.99, 100),
        ("Webcam ๐Ÿ“น", 79.99, 5)
    ]
    
    cursor.executemany(
        "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
        test_products
    )
    
    conn.commit()
    yield conn
    conn.close()

# ๐ŸŽฏ Test successful order
def test_create_order_success(test_database):
    order_system = OrderSystem(test_database)
    
    # ๐Ÿ›๏ธ Create an order
    order_id = order_system.create_order(
        customer_id=123,
        items=[
            {'product_id': 1, 'quantity': 2},  # 2 keyboards
            {'product_id': 3, 'quantity': 3}   # 3 cables
        ]
    )
    
    # โœ… Verify order was created
    cursor = test_database.cursor()
    cursor.execute("SELECT total, status FROM orders WHERE id = ?", (order_id,))
    total, status = cursor.fetchone()
    
    expected_total = (89.99 * 2) + (12.99 * 3)
    assert abs(total - expected_total) < 0.01  # Float comparison
    assert status == 'pending'
    
    # ๐Ÿ“ฆ Verify stock was updated
    cursor.execute("SELECT stock FROM products WHERE id IN (1, 3)")
    stocks = cursor.fetchall()
    assert stocks[0][0] == 18  # 20 - 2 keyboards
    assert stocks[1][0] == 97  # 100 - 3 cables

# ๐Ÿ˜ฑ Test insufficient stock
def test_create_order_insufficient_stock(test_database):
    order_system = OrderSystem(test_database)
    
    # ๐Ÿšซ Try to order more than available
    with pytest.raises(ValueError) as exc_info:
        order_system.create_order(
            customer_id=456,
            items=[{'product_id': 4, 'quantity': 10}]  # Only 5 webcams!
        )
    
    assert "Insufficient stock!" in str(exc_info.value)
    assert "Only 5 available" in str(exc_info.value)
    
    # โœ… Verify stock wasn't changed
    cursor = test_database.cursor()
    cursor.execute("SELECT stock FROM products WHERE id = 4")
    assert cursor.fetchone()[0] == 5  # Stock unchanged! ๐Ÿ›ก๏ธ

๐ŸŽฏ Try it yourself: Add a test for canceling orders and restoring stock!

๐ŸŽฎ Example 2: User Authentication System

Letโ€™s test a user authentication system with proper database isolation:

import hashlib
import secrets
from datetime import datetime, timedelta
import pytest

# ๐Ÿ” Authentication system
class AuthSystem:
    def __init__(self, db_connection):
        self.conn = db_connection
        self.cursor = db_connection.cursor()
    
    # ๐Ÿ”’ Hash password securely
    def _hash_password(self, password, salt):
        return hashlib.pbkdf2_hmac(
            'sha256',
            password.encode('utf-8'),
            salt.encode('utf-8'),
            100000
        ).hex()
    
    # ๐Ÿ‘ค Register new user
    def register_user(self, username, email, password):
        # ๐ŸŽฒ Generate salt
        salt = secrets.token_hex(32)
        password_hash = self._hash_password(password, salt)
        
        try:
            self.cursor.execute("""
                INSERT INTO users (username, email, password_hash, salt, created_at)
                VALUES (?, ?, ?, ?, ?)
            """, (username, email, password_hash, salt, datetime.now()))
            
            self.conn.commit()
            return True, "Registration successful! ๐ŸŽ‰"
        except sqlite3.IntegrityError as e:
            if "username" in str(e):
                return False, "Username already taken! ๐Ÿ˜•"
            elif "email" in str(e):
                return False, "Email already registered! ๐Ÿ“ง"
            raise
    
    # ๐Ÿ”“ Login user
    def login(self, username, password):
        self.cursor.execute("""
            SELECT id, password_hash, salt, failed_attempts, locked_until
            FROM users WHERE username = ?
        """, (username,))
        
        user = self.cursor.fetchone()
        if not user:
            return False, "Invalid credentials! ๐Ÿšซ"
        
        user_id, password_hash, salt, failed_attempts, locked_until = user
        
        # ๐Ÿ”’ Check if account is locked
        if locked_until and datetime.fromisoformat(locked_until) > datetime.now():
            return False, "Account locked! Try again later ๐Ÿ”’"
        
        # โœ… Verify password
        if self._hash_password(password, salt) == password_hash:
            # ๐ŸŽ‰ Reset failed attempts
            self.cursor.execute("""
                UPDATE users 
                SET failed_attempts = 0, locked_until = NULL, last_login = ?
                WHERE id = ?
            """, (datetime.now(), user_id))
            self.conn.commit()
            return True, "Welcome back! ๐Ÿ‘‹"
        else:
            # ๐Ÿ˜ฑ Track failed attempt
            failed_attempts += 1
            locked_until = None
            
            if failed_attempts >= 3:
                locked_until = datetime.now() + timedelta(minutes=15)
                message = "Too many attempts! Account locked for 15 minutes ๐Ÿ”’"
            else:
                message = f"Invalid credentials! {3 - failed_attempts} attempts remaining โš ๏ธ"
            
            self.cursor.execute("""
                UPDATE users 
                SET failed_attempts = ?, locked_until = ?
                WHERE id = ?
            """, (failed_attempts, locked_until, user_id))
            self.conn.commit()
            
            return False, message

# ๐Ÿงช Test database setup
@pytest.fixture
def auth_test_db():
    conn = sqlite3.connect(':memory:')
    cursor = conn.cursor()
    
    # ๐Ÿ‘ฅ Create users table
    cursor.execute("""
        CREATE TABLE users (
            id INTEGER PRIMARY KEY,
            username TEXT UNIQUE NOT NULL,
            email TEXT UNIQUE NOT NULL,
            password_hash TEXT NOT NULL,
            salt TEXT NOT NULL,
            failed_attempts INTEGER DEFAULT 0,
            locked_until TEXT,
            created_at TIMESTAMP,
            last_login TIMESTAMP
        )
    """)
    
    conn.commit()
    yield conn
    conn.close()

# โœ… Test successful registration
def test_register_success(auth_test_db):
    auth = AuthSystem(auth_test_db)
    
    success, message = auth.register_user(
        "python_lover",
        "[email protected]",
        "SecurePass123! ๐Ÿ”"
    )
    
    assert success is True
    assert "successful" in message
    
    # ๐Ÿ” Verify user was created
    cursor = auth_test_db.cursor()
    cursor.execute("SELECT username, email FROM users WHERE username = ?", ("python_lover",))
    user = cursor.fetchone()
    assert user[0] == "python_lover"
    assert user[1] == "[email protected]"

# โŒ Test duplicate username
def test_register_duplicate_username(auth_test_db):
    auth = AuthSystem(auth_test_db)
    
    # ๐Ÿ‘ค First registration
    auth.register_user("coder123", "[email protected]", "Pass123!")
    
    # ๐Ÿšซ Try same username
    success, message = auth.register_user(
        "coder123",  # Same username!
        "[email protected]",
        "DifferentPass!"
    )
    
    assert success is False
    assert "Username already taken" in message

# ๐Ÿ” Test login and lockout
def test_login_with_lockout(auth_test_db):
    auth = AuthSystem(auth_test_db)
    
    # ๐Ÿ‘ค Register user
    auth.register_user("test_user", "[email protected]", "CorrectPass123!")
    
    # โŒ Try wrong password 3 times
    for i in range(3):
        success, message = auth.login("test_user", "WrongPass!")
        assert success is False
        
        if i < 2:
            assert f"{2-i} attempts remaining" in message
        else:
            assert "Account locked" in message
    
    # ๐Ÿ”’ Try correct password while locked
    success, message = auth.login("test_user", "CorrectPass123!")
    assert success is False
    assert "Account locked" in message
    
    # โœ… Successful login (in real scenario, wait 15 minutes)
    # For testing, we'll manually unlock
    cursor = auth_test_db.cursor()
    cursor.execute("UPDATE users SET locked_until = NULL WHERE username = ?", ("test_user",))
    auth_test_db.commit()
    
    success, message = auth.login("test_user", "CorrectPass123!")
    assert success is True
    assert "Welcome back" in message

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Database Migrations in Tests

When testing evolving schemas, manage migrations properly:

import alembic.config
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# ๐ŸŽฏ Advanced test database with migrations
class TestDatabaseManager:
    def __init__(self, db_url="sqlite:///:memory:"):
        self.db_url = db_url
        self.engine = None
        self.SessionLocal = None
    
    # ๐Ÿ—๏ธ Set up database with migrations
    def setup(self):
        self.engine = create_engine(self.db_url)
        
        # ๐Ÿ”„ Run migrations
        alembic_cfg = alembic.config.Config("alembic.ini")
        alembic_cfg.set_main_option("sqlalchemy.url", self.db_url)
        alembic.command.upgrade(alembic_cfg, "head")
        
        self.SessionLocal = sessionmaker(bind=self.engine)
        return self
    
    # ๐Ÿงน Clean up
    def teardown(self):
        if self.engine:
            self.engine.dispose()
    
    # ๐ŸŽจ Get a test session
    def get_session(self):
        session = self.SessionLocal()
        try:
            yield session
            session.commit()
        except Exception:
            session.rollback()
            raise
        finally:
            session.close()

# ๐Ÿงช Use in tests
@pytest.fixture(scope="function")
def test_db_manager():
    manager = TestDatabaseManager()
    manager.setup()
    yield manager
    manager.teardown()

๐Ÿ—๏ธ Test Data Factories

Create realistic test data easily:

from faker import Faker
import factory
from factory.alchemy import SQLAlchemyModelFactory

fake = Faker()

# ๐Ÿญ User factory
class UserFactory(SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session_persistence = 'commit'
    
    username = factory.LazyAttribute(lambda x: fake.user_name())
    email = factory.LazyAttribute(lambda x: fake.email())
    full_name = factory.LazyAttribute(lambda x: fake.name())
    created_at = factory.LazyFunction(datetime.now)

# ๐ŸŽฎ Product factory
class ProductFactory(SQLAlchemyModelFactory):
    class Meta:
        model = Product
    
    name = factory.Sequence(lambda n: f"Product {n} {fake.emoji()}")
    price = factory.LazyFunction(lambda: round(fake.random.uniform(10, 200), 2))
    stock = factory.LazyFunction(lambda: fake.random_int(0, 100))
    category = factory.Faker('random_element', elements=['Electronics ๐Ÿ’ป', 'Books ๐Ÿ“š', 'Games ๐ŸŽฎ'])

# ๐Ÿงช Use in tests
def test_bulk_order_processing(test_session):
    # ๐ŸŽจ Create test data quickly!
    users = UserFactory.create_batch(5)
    products = ProductFactory.create_batch(20)
    
    # ๐Ÿ›’ Test bulk operations
    for user in users:
        # Create orders with random products
        selected_products = fake.random_elements(products, length=3)
        # ... test order processing

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Shared Test Database State

# โŒ Wrong way - tests affect each other!
class TestProduct:
    db = setup_test_database()  # Shared across all tests! ๐Ÿ˜ฐ
    
    def test_create_product(self):
        # This creates data that affects other tests
        create_product(self.db, "Test Product")
    
    def test_list_products(self):
        # This might see products from other tests! ๐Ÿ’ฅ
        products = list_products(self.db)
        assert len(products) == 0  # Fails if test_create_product ran first!

# โœ… Correct way - isolated test databases!
@pytest.fixture
def test_db():
    db = setup_test_database()
    yield db
    cleanup_database(db)  # ๐Ÿงน Clean after each test

def test_create_product(test_db):
    create_product(test_db, "Test Product")
    # Database is fresh for this test only! โœจ

def test_list_products(test_db):
    products = list_products(test_db)
    assert len(products) == 0  # Always passes! ๐ŸŽ‰

๐Ÿคฏ Pitfall 2: Forgetting to Clean Up

# โŒ Dangerous - leaves test data behind!
def test_user_creation():
    conn = psycopg2.connect("postgresql://localhost/main_db")  # ๐Ÿ˜ฑ Production DB!
    cursor = conn.cursor()
    cursor.execute("INSERT INTO users (email) VALUES ('[email protected]')")
    conn.commit()
    # Oops! Test data in production! ๐Ÿ’ฅ

# โœ… Safe - use test database with cleanup!
@pytest.fixture
def test_db():
    # ๐Ÿ›ก๏ธ Create isolated test database
    conn = psycopg2.connect("postgresql://localhost/test_db")
    
    yield conn
    
    # ๐Ÿงน Clean up everything
    conn.rollback()  # Undo any uncommitted changes
    conn.close()

def test_user_creation(test_db):
    cursor = test_db.cursor()
    cursor.execute("INSERT INTO users (email) VALUES ('[email protected]')")
    # No commit needed - will be rolled back! โœ…

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Use Transactions: Rollback after each test for speed and isolation
  2. ๐Ÿ“ Seed Consistent Data: Use factories or fixtures for predictable test data
  3. ๐Ÿ›ก๏ธ Never Touch Production: Always use separate test databases
  4. ๐ŸŽจ Name Test Databases Clearly: Use prefixes like test_ or _test
  5. โœจ Keep Tests Fast: Use in-memory databases when possible
  6. ๐Ÿ”„ Reset Between Tests: Start each test with a clean slate
  7. ๐Ÿ“Š Use Migrations: Test your schema changes too!

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Library Management System Test Suite

Create a comprehensive test suite for a library system:

๐Ÿ“‹ Requirements:

  • โœ… Books with ISBN, title, author, and availability
  • ๐Ÿ“š Members who can borrow books
  • ๐Ÿ”„ Borrowing system with due dates
  • ๐Ÿ’ฐ Fine calculation for overdue books
  • ๐Ÿ“Š Reports on most borrowed books
  • ๐ŸŽจ Each book category needs an emoji!

๐Ÿš€ Bonus Points:

  • Implement reservation system
  • Add book recommendations based on history
  • Create admin functions for managing fines

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
import sqlite3
from datetime import datetime, timedelta
from decimal import Decimal
import pytest

# ๐Ÿ“š Library Management System
class Library:
    def __init__(self, db_connection):
        self.conn = db_connection
        self.cursor = db_connection.cursor()
        self.fine_per_day = Decimal('0.50')
    
    # ๐Ÿ“– Add a new book
    def add_book(self, isbn, title, author, category):
        emoji_map = {
            'fiction': '๐Ÿ“–',
            'science': '๐Ÿ”ฌ',
            'history': '๐Ÿ“œ',
            'technology': '๐Ÿ’ป',
            'children': '๐Ÿงธ'
        }
        
        self.cursor.execute("""
            INSERT INTO books (isbn, title, author, category, emoji, available)
            VALUES (?, ?, ?, ?, ?, 1)
        """, (isbn, title, author, category, emoji_map.get(category, '๐Ÿ“š')))
        self.conn.commit()
        
        return self.cursor.lastrowid
    
    # ๐Ÿ‘ค Register a member
    def register_member(self, name, email):
        self.cursor.execute("""
            INSERT INTO members (name, email, joined_date, active)
            VALUES (?, ?, ?, 1)
        """, (name, email, datetime.now()))
        self.conn.commit()
        
        return self.cursor.lastrowid
    
    # ๐Ÿ“– Borrow a book
    def borrow_book(self, member_id, isbn, days=14):
        # ๐Ÿ” Check if book is available
        self.cursor.execute("""
            SELECT id, title, available FROM books WHERE isbn = ?
        """, (isbn,))
        
        book = self.cursor.fetchone()
        if not book:
            raise ValueError("Book not found! ๐Ÿ˜•")
        
        book_id, title, available = book
        if not available:
            raise ValueError(f"'{title}' is not available! ๐Ÿ“š")
        
        # ๐Ÿ” Check member's current loans
        self.cursor.execute("""
            SELECT COUNT(*) FROM loans 
            WHERE member_id = ? AND returned_date IS NULL
        """, (member_id,))
        
        active_loans = self.cursor.fetchone()[0]
        if active_loans >= 3:
            raise ValueError("Maximum 3 books allowed! ๐Ÿ“š๐Ÿ“š๐Ÿ“š")
        
        # โœ… Create loan
        due_date = datetime.now() + timedelta(days=days)
        self.cursor.execute("""
            INSERT INTO loans (book_id, member_id, loan_date, due_date)
            VALUES (?, ?, ?, ?)
        """, (book_id, member_id, datetime.now(), due_date))
        
        # ๐Ÿ“‰ Mark book as unavailable
        self.cursor.execute("""
            UPDATE books SET available = 0 WHERE id = ?
        """, (book_id,))
        
        self.conn.commit()
        return f"Borrowed '{title}'! Due: {due_date.strftime('%Y-%m-%d')} ๐Ÿ“…"
    
    # ๐Ÿ“š Return a book
    def return_book(self, member_id, isbn):
        # ๐Ÿ” Find the loan
        self.cursor.execute("""
            SELECT l.id, l.due_date, b.title
            FROM loans l
            JOIN books b ON l.book_id = b.id
            WHERE b.isbn = ? AND l.member_id = ? AND l.returned_date IS NULL
        """, (isbn, member_id))
        
        loan = self.cursor.fetchone()
        if not loan:
            raise ValueError("No active loan found! ๐Ÿค”")
        
        loan_id, due_date, title = loan
        due_date = datetime.fromisoformat(due_date)
        
        # ๐Ÿ’ฐ Calculate fine if overdue
        fine = Decimal('0')
        if datetime.now() > due_date:
            days_overdue = (datetime.now() - due_date).days
            fine = self.fine_per_day * days_overdue
        
        # โœ… Complete return
        self.cursor.execute("""
            UPDATE loans 
            SET returned_date = ?, fine = ?
            WHERE id = ?
        """, (datetime.now(), float(fine), loan_id))
        
        # ๐Ÿ“ˆ Mark book as available
        self.cursor.execute("""
            UPDATE books SET available = 1 
            WHERE isbn = ?
        """, (isbn,))
        
        self.conn.commit()
        
        if fine > 0:
            return f"'{title}' returned. Fine: ${fine:.2f} ๐Ÿ’ธ"
        return f"'{title}' returned on time! ๐ŸŽ‰"
    
    # ๐Ÿ“Š Get most borrowed books
    def get_popular_books(self, limit=5):
        self.cursor.execute("""
            SELECT b.title, b.author, b.emoji, COUNT(l.id) as borrow_count
            FROM books b
            JOIN loans l ON b.id = l.book_id
            GROUP BY b.id
            ORDER BY borrow_count DESC
            LIMIT ?
        """, (limit,))
        
        return self.cursor.fetchall()

# ๐Ÿงช Test database fixture
@pytest.fixture
def library_db():
    conn = sqlite3.connect(':memory:')
    cursor = conn.cursor()
    
    # ๐Ÿ“Š Create schema
    cursor.executescript("""
        CREATE TABLE books (
            id INTEGER PRIMARY KEY,
            isbn TEXT UNIQUE NOT NULL,
            title TEXT NOT NULL,
            author TEXT NOT NULL,
            category TEXT,
            emoji TEXT,
            available INTEGER DEFAULT 1
        );
        
        CREATE TABLE members (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            email TEXT UNIQUE NOT NULL,
            joined_date TIMESTAMP,
            active INTEGER DEFAULT 1
        );
        
        CREATE TABLE loans (
            id INTEGER PRIMARY KEY,
            book_id INTEGER REFERENCES books(id),
            member_id INTEGER REFERENCES members(id),
            loan_date TIMESTAMP NOT NULL,
            due_date TIMESTAMP NOT NULL,
            returned_date TIMESTAMP,
            fine REAL DEFAULT 0
        );
    """)
    
    conn.commit()
    yield conn
    conn.close()

# โœ… Test successful book borrowing
def test_borrow_book_success(library_db):
    library = Library(library_db)
    
    # ๐Ÿ“š Add a book
    library.add_book("978-0-13-110362-7", "The Python Book", "Guido van Rossum", "technology")
    
    # ๐Ÿ‘ค Register a member
    member_id = library.register_member("Alice Python", "[email protected]")
    
    # ๐Ÿ“– Borrow the book
    result = library.borrow_book(member_id, "978-0-13-110362-7")
    
    assert "Borrowed 'The Python Book'" in result
    assert "Due:" in result
    
    # โœ… Verify book is now unavailable
    cursor = library_db.cursor()
    cursor.execute("SELECT available FROM books WHERE isbn = ?", ("978-0-13-110362-7",))
    assert cursor.fetchone()[0] == 0

# ๐Ÿ˜ฑ Test borrowing limit
def test_borrow_limit_exceeded(library_db):
    library = Library(library_db)
    
    # ๐Ÿ‘ค Create member
    member_id = library.register_member("Bob Reader", "[email protected]")
    
    # ๐Ÿ“š Add and borrow 3 books
    for i in range(3):
        isbn = f"978-0-{i:02d}-000000-0"
        library.add_book(isbn, f"Book {i+1}", "Author", "fiction")
        library.borrow_book(member_id, isbn)
    
    # ๐Ÿšซ Try to borrow 4th book
    library.add_book("978-0-99-000000-0", "One Too Many", "Author", "fiction")
    
    with pytest.raises(ValueError) as exc_info:
        library.borrow_book(member_id, "978-0-99-000000-0")
    
    assert "Maximum 3 books allowed" in str(exc_info.value)

# ๐Ÿ’ฐ Test overdue fines
def test_overdue_fine_calculation(library_db):
    library = Library(library_db)
    
    # Setup
    library.add_book("978-1-23-456789-0", "Always Late", "Procrastinator", "fiction")
    member_id = library.register_member("Charlie Late", "[email protected]")
    
    # ๐Ÿ“… Manually set loan date to past
    cursor = library_db.cursor()
    past_date = datetime.now() - timedelta(days=20)
    due_date = past_date + timedelta(days=14)  # 6 days overdue
    
    cursor.execute("""
        INSERT INTO loans (book_id, member_id, loan_date, due_date)
        VALUES (1, ?, ?, ?)
    """, (member_id, past_date, due_date))
    
    cursor.execute("UPDATE books SET available = 0 WHERE id = 1")
    library_db.commit()
    
    # ๐Ÿ’ธ Return late book
    result = library.return_book(member_id, "978-1-23-456789-0")
    
    assert "Fine: $3.00" in result  # 6 days ร— $0.50
    
    # โœ… Verify fine was recorded
    cursor.execute("SELECT fine FROM loans WHERE member_id = ?", (member_id,))
    assert cursor.fetchone()[0] == 3.0

# ๐Ÿ“Š Test popular books report
def test_popular_books_report(library_db):
    library = Library(library_db)
    
    # ๐Ÿ“š Create books and members
    books = [
        ("978-0-00-000001-0", "Python Mastery", "Expert Dev", "technology"),
        ("978-0-00-000002-0", "Web Dev 101", "Full Stack", "technology"),
        ("978-0-00-000003-0", "Data Science", "ML Expert", "science")
    ]
    
    for isbn, title, author, category in books:
        library.add_book(isbn, title, author, category)
    
    # ๐Ÿ‘ฅ Create borrowing history
    for i in range(3):
        member_id = library.register_member(f"Member {i}", f"member{i}@lib.com")
        
        # Python book is most popular (borrowed by all)
        library.borrow_book(member_id, "978-0-00-000001-0")
        library.return_book(member_id, "978-0-00-000001-0")
        
        # Web Dev borrowed by 2 members
        if i < 2:
            library.borrow_book(member_id, "978-0-00-000002-0")
            library.return_book(member_id, "978-0-00-000002-0")
    
    # ๐Ÿ“Š Get report
    popular = library.get_popular_books(limit=2)
    
    assert len(popular) == 2
    assert popular[0][0] == "Python Mastery"  # Most borrowed
    assert popular[0][3] == 3  # Borrowed 3 times
    assert popular[1][0] == "Web Dev 101"
    assert popular[1][3] == 2  # Borrowed 2 times

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Set up test databases for safe, isolated testing ๐Ÿ’ช
  • โœ… Write reliable database tests that donโ€™t interfere with each other ๐Ÿ›ก๏ธ
  • โœ… Use fixtures and factories for consistent test data ๐ŸŽฏ
  • โœ… Handle transactions properly to keep tests fast and clean ๐Ÿ›
  • โœ… Test complex database operations with confidence! ๐Ÿš€

Remember: Test databases are your safety netโ€”use them liberally to catch bugs before they reach production! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered testing database code with test databases!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the library management exercise above
  2. ๐Ÿ—๏ธ Set up test databases for your current projects
  3. ๐Ÿ“š Learn about database mocking for unit tests
  4. ๐ŸŒŸ Explore continuous integration with database tests

Keep testing, keep learning, and most importantly, keep your data safe! ๐Ÿš€


Happy testing! ๐ŸŽ‰๐Ÿš€โœจ