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:
- Isolation ๐๏ธ: Each test runs in its own clean environment
- Reproducibility ๐: Tests produce consistent results every time
- Safety ๐ก๏ธ: No risk of corrupting production data
- Speed โก: Tests can run in parallel without interference
- 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
- ๐ฏ Use Transactions: Rollback after each test for speed and isolation
- ๐ Seed Consistent Data: Use factories or fixtures for predictable test data
- ๐ก๏ธ Never Touch Production: Always use separate test databases
- ๐จ Name Test Databases Clearly: Use prefixes like
test_
or_test
- โจ Keep Tests Fast: Use in-memory databases when possible
- ๐ Reset Between Tests: Start each test with a clean slate
- ๐ 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:
- ๐ป Practice with the library management exercise above
- ๐๏ธ Set up test databases for your current projects
- ๐ Learn about database mocking for unit tests
- ๐ Explore continuous integration with database tests
Keep testing, keep learning, and most importantly, keep your data safe! ๐
Happy testing! ๐๐โจ