數據庫測試
數據庫測試的基本概念
數據庫測試是整合測試的重要組成部分,專注於驗證應用程序與數據庫之間的交互是否正確。在 FastAPI 應用中,數據庫測試確保 ORM 模型、存儲庫(repositories)和數據訪問層能夠正確執行 CRUD(創建、讀取、更新、刪除)操作。
概念 | 說明 |
---|---|
測試範圍 | 數據庫模型、查詢和事務操作 |
隔離程度 | 使用專用測試數據庫或內存數據庫 |
執行速度 | 中等,受數據庫性能影響 |
測試數據 | 需要準備測試數據和清理機制 |
事務管理 | 使用事務確保測試之間的隔離 |
數據庫測試的挑戰與解決方案
挑戰 | 解決方案 |
---|---|
測試隔離 | 使用專用測試數據庫或內存數據庫 |
測試速度 | 使用內存數據庫加速測試執行 |
數據準備 | 使用夾具(fixtures)或工廠模式創建測試數據 |
數據清理 | 使用事務回滾或測試後清理策略 |
並行執行 | 確保測試可以並行運行而不互相干擾 |
設置測試數據庫環境
測試數據庫策略
在 FastAPI 應用中,有幾種常用的測試數據庫策略:
策略 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
專用測試數據庫 | 與生產環境相似 可測試數據庫特定功能 |
設置複雜 測試較慢 |
完整的集成測試 測試數據庫特定功能 |
內存數據庫 | 速度快 無需外部依賴 |
可能缺少某些功能 與生產環境差異 |
單元測試 快速整合測試 |
測試容器 | 隔離性好 環境一致性 |
需要 Docker 資源消耗較大 |
CI/CD 環境 完整的集成測試 |
使用 SQLite 內存數據庫進行測試
SQLite 內存數據庫是快速測試的理想選擇:
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base
from app.main import app
from app.dependencies import get_db
# 創建內存數據庫引擎
TEST_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(TEST_DATABASE_URL, connect_args={"check_same_thread": False})
# 創建測試會話工廠
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="function")
def db():
# 創建所有表
Base.metadata.create_all(bind=engine)
# 創建數據庫會話
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# 清理 - 刪除所有表
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def client(db):
# 覆蓋依賴
def override_get_db():
try:
yield db
finally:
pass
app.dependency_overrides[get_db] = override_get_db
from fastapi.testclient import TestClient
with TestClient(app) as c:
yield c
# 清理依賴覆蓋
app.dependency_overrides = {}
測試數據準備
使用夾具創建測試數據
測試夾具(fixtures)是準備測試數據的理想方式:
# tests/conftest.py
import pytest
from app.models import User, Item
from app.schemas import UserCreate, ItemCreate
from app.services.user_service import UserService
from app.services.item_service import ItemService
@pytest.fixture
def user_service(db):
return UserService(db)
@pytest.fixture
def item_service(db):
return ItemService(db)
@pytest.fixture
def test_user(user_service):
user_data = UserCreate(
username="testuser",
email="test@example.com",
password="password123"
)
user = user_service.create_user(user_data)
return user
@pytest.fixture
def test_items(item_service, test_user):
items = []
# 創建測試商品
for i in range(3):
item_data = ItemCreate(
name=f"測試商品 {i}",
description=f"這是測試商品 {i} 的描述",
price=10.0 * (i + 1),
owner_id=test_user.id
)
items.append(item_service.create_item(item_data))
return items
測試數據庫模型
測試模型關係和約束
# tests/models/test_models.py
import pytest
from sqlalchemy.exc import IntegrityError
from app.models import User, Item
def test_user_model(db):
# 創建用戶
user = User(username="testuser", email="test@example.com", hashed_password="hashedpw")
db.add(user)
db.commit()
# 驗證用戶被正確創建
assert user.id is not None
assert user.username == "testuser"
assert user.email == "test@example.com"
# 測試唯一性約束
duplicate_user = User(username="testuser", email="test@example.com", hashed_password="hashedpw")
db.add(duplicate_user)
with pytest.raises(IntegrityError):
db.commit()
db.rollback()
def test_item_model(db):
# 創建用戶和商品
user = User(username="itemowner", email="owner@example.com", hashed_password="hashedpw")
db.add(user)
db.commit()
item = Item(name="測試商品", price=99.99, owner_id=user.id)
db.add(item)
db.commit()
# 驗證商品被正確創建
assert item.id is not None
assert item.name == "測試商品"
assert item.price == 99.99
assert item.owner_id == user.id
def test_user_items_relationship(db):
# 創建用戶
user = User(username="relationuser", email="relation@example.com", hashed_password="hashedpw")
db.add(user)
db.commit()
# 創建多個商品
items = [
Item(name="商品1", price=10.0, owner_id=user.id),
Item(name="商品2", price=20.0, owner_id=user.id)
]
db.add_all(items)
db.commit()
# 驗證關係
db.refresh(user)
assert len(user.items) == 2
assert user.items[0].name == "商品1"
assert user.items[1].name == "商品2"
# 驗證反向關係
assert items[0].owner.username == "relationuser"
測試存儲庫(Repository)
存儲庫模式是一種常見的數據訪問模式,它封裝了數據庫操作邏輯。
基本的存儲庫測試
# tests/repositories/test_user_repository.py
import pytest
from app.repositories.user_repository import UserRepository
from app.models import User
@pytest.fixture
def user_repo(db):
return UserRepository(db)
def test_create_user(user_repo):
# 創建用戶
user_data = {
"username": "repouser",
"email": "repo@example.com",
"hashed_password": "hashedpassword"
}
user = user_repo.create(user_data)
# 驗證用戶被創建
assert user.id is not None
assert user.username == user_data["username"]
assert user.email == user_data["email"]
def test_get_user_by_id(user_repo):
# 創建用戶
user_data = {
"username": "getuser",
"email": "get@example.com",
"hashed_password": "hashedpassword"
}
created_user = user_repo.create(user_data)
# 獲取用戶
user = user_repo.get_by_id(created_user.id)
# 驗證
assert user is not None
assert user.id == created_user.id
assert user.username == user_data["username"]
def test_update_user(user_repo):
# 創建用戶
user_data = {
"username": "updateuser",
"email": "update@example.com",
"hashed_password": "hashedpassword"
}
user = user_repo.create(user_data)
# 更新用戶
update_data = {
"username": "updateduser"
}
updated_user = user_repo.update(user.id, update_data)
# 驗證
assert updated_user.id == user.id
assert updated_user.username == update_data["username"]
assert updated_user.email == user_data["email"] # 未更新的字段保持不變
def test_delete_user(user_repo):
# 創建用戶
user_data = {
"username": "deleteuser",
"email": "delete@example.com",
"hashed_password": "hashedpassword"
}
user = user_repo.create(user_data)
# 刪除用戶
result = user_repo.delete(user.id)
# 驗證
assert result is True
assert user_repo.get_by_id(user.id) is None
測試複雜查詢
對於複雜的數據庫查詢,需要準備更複雜的測試數據和更全面的驗證。
測試分頁和過濾
# tests/repositories/test_item_repository.py
import pytest
from app.repositories.item_repository import ItemRepository
from app.repositories.user_repository import UserRepository
@pytest.fixture
def repos(db):
return {
"user": UserRepository(db),
"item": ItemRepository(db)
}
@pytest.fixture
def test_data(repos):
# 創建測試用戶
user1 = repos["user"].create({
"username": "user1",
"email": "user1@example.com",
"hashed_password": "hashedpw"
})
# 創建測試商品
items = []
# 用戶1的商品
for i in range(5):
items.append(repos["item"].create({
"name": f"商品{i}",
"price": 10.0 * (i + 1),
"category": "電子產品" if i % 2 == 0 else "家居",
"owner_id": user1.id
}))
return {"user": user1, "items": items}
def test_get_items_with_pagination(repos, test_data):
# 測試分頁
items_page1 = repos["item"].get_all(skip=0, limit=3)
items_page2 = repos["item"].get_all(skip=3, limit=3)
assert len(items_page1) == 3
assert len(items_page2) == 2 # 總共5個商品,第二頁只有2個
# 驗證分頁數據不重複
page1_ids = [item.id for item in items_page1]
page2_ids = [item.id for item in items_page2]
assert not any(item_id in page1_ids for item_id in page2_ids)
def test_get_items_by_category(repos, test_data):
# 獲取電子產品類別的商品
electronics = repos["item"].get_by_category("電子產品")
assert len(electronics) == 3
assert all(item.category == "電子產品" for item in electronics)
# 獲取家居類別的商品
home_items = repos["item"].get_by_category("家居")
assert len(home_items) == 2
assert all(item.category == "家居" for item in home_items)
測試事務和並發
測試數據庫事務和並發操作是確保數據一致性的重要部分。
測試事務操作
# tests/repositories/test_transaction.py
import pytest
from app.repositories.order_repository import OrderRepository
from app.models import User, Item, Order
@pytest.fixture
def order_repo(db):
return OrderRepository(db)
@pytest.fixture
def setup_order_data(db):
# 創建用戶
buyer = User(username="buyer", email="buyer@example.com", hashed_password="hashedpw")
seller = User(username="seller", email="seller@example.com", hashed_password="hashedpw")
db.add_all([buyer, seller])
db.commit()
# 創建商品
item = Item(
name="測試商品",
price=100.0,
inventory=5,
owner_id=seller.id
)
db.add(item)
db.commit()
return {"buyer": buyer, "seller": seller, "item": item}
def test_create_order_success(order_repo, setup_order_data):
buyer = setup_order_data["buyer"]
item = setup_order_data["item"]
# 執行訂單創建
order = order_repo.create_order(
buyer_id=buyer.id,
item_id=item.id,
quantity=2,
amount=item.price * 2
)
# 驗證訂單記錄
assert order.id is not None
assert order.buyer_id == buyer.id
assert order.item_id == item.id
assert order.quantity == 2
assert order.amount == 200.0
assert order.status == "completed"
# 驗證庫存更新
assert item.inventory == 3 # 原庫存5,購買2個
def test_create_order_insufficient_inventory(order_repo, setup_order_data):
buyer = setup_order_data["buyer"]
item = setup_order_data["item"]
# 嘗試購買超過庫存的數量
with pytest.raises(ValueError) as exc_info:
order_repo.create_order(
buyer_id=buyer.id,
item_id=item.id,
quantity=10, # 庫存只有5個
amount=item.price * 10
)
assert "庫存不足" in str(exc_info.value)
# 驗證庫存未變化
assert item.inventory == 5
測試數據庫遷移
測試數據庫遷移確保模式變更不會破壞現有功能。
簡單的遷移測試
# tests/migrations/test_migrations.py
import subprocess
import pytest
from sqlalchemy import inspect
from app.database import engine
def test_migrations_apply_successfully():
# 運行遷移命令
result = subprocess.run(
["alembic", "upgrade", "head"],
capture_output=True,
text=True
)
# 檢查命令是否成功執行
assert result.returncode == 0, f"遷移失敗: {result.stderr}"
def test_table_structure():
# 獲取檢查器
inspector = inspect(engine)
# 檢查表是否存在
tables = inspector.get_table_names()
assert "users" in tables
assert "items" in tables
# 檢查列
user_columns = {col["name"] for col in inspector.get_columns("users")}
assert "id" in user_columns
assert "username" in user_columns
assert "email" in user_columns
assert "hashed_password" in user_columns
測試數據庫性能
對於性能關鍵的應用,測試數據庫查詢性能是很重要的。
簡單的性能測試
# tests/performance/test_db_performance.py
import time
import pytest
from app.repositories.item_repository import ItemRepository
from app.models import User, Item
@pytest.fixture
def setup_performance_data(db):
# 創建測試用戶
user = User(username="perfuser", email="perf@example.com", hashed_password="hashedpw")
db.add(user)
db.commit()
# 創建測試商品
items = []
for i in range(50):
item = Item(
name=f"性能測試商品 {i}",
price=10.0 * (i % 10 + 1),
category="電子產品" if i % 3 == 0 else "家居" if i % 3 == 1 else "辦公用品",
owner_id=user.id
)
items.append(item)
db.add_all(items)
db.commit()
return {"user": user, "items": items}
@pytest.fixture
def item_repo(db):
return ItemRepository(db)
def test_get_all_items_performance(item_repo, setup_performance_data):
# 測量獲取所有商品的性能
start_time = time.time()
items = item_repo.get_all(limit=100)
end_time = time.time()
duration = end_time - start_time
assert len(items) >= 50
assert duration < 0.1 # 期望查詢時間小於 100ms
def test_filter_by_category_performance(item_repo, setup_performance_data):
# 測量按類別過濾的性能
start_time = time.time()
items = item_repo.get_by_category("電子產品")
end_time = time.time()
duration = end_time - start_time
assert len(items) > 0
assert duration < 0.05 # 期望查詢時間小於 50ms
數據庫測試的最佳實踐
測試隔離
實踐 | 說明 |
---|---|
獨立測試數據庫 | 使用專用的測試數據庫,避免干擾生產數據 |
測試夾具作用域 | 合理設置夾具作用域,平衡性能和隔離性 |
事務回滾 | 使用事務確保測試之間的隔離 |
清理測試數據 | 測試後清理創建的數據,避免數據積累 |
測試數據管理
實踐 | 說明 |
---|---|
工廠模式 | 使用工廠模式生成測試數據,提高可維護性 |
參數化測試 | 使用不同的數據集測試相同的功能 |
測試數據生成器 | 使用工具自動生成大量測試數據 |
真實數據子集 | 在某些情況下使用生產數據的子集進行測試 |
數據庫性能測試
實踐 | 說明 |
---|---|
查詢性能基準 | 設置查詢性能的基準和閾值 |
索引測試 | 測試索引對查詢性能的影響 |
大數據集測試 | 使用大數據集測試系統在負載下的性能 |
慢查詢識別 | 識別並優化慢查詢 |
常見的數據庫測試模式
存儲庫模式測試
存儲庫模式是一種常見的數據訪問模式,它封裝了數據庫操作邏輯。測試存儲庫確保數據訪問層正確工作。
# app/repositories/base_repository.py
class BaseRepository:
def __init__(self, db):
self.db = db
def get_by_id(self, model, id):
return self.db.query(model).filter(model.id == id).first()
def get_all(self, model, skip=0, limit=100):
return self.db.query(model).offset(skip).limit(limit).all()
def create(self, model, data):
instance = model(**data)
self.db.add(instance)
self.db.commit()
self.db.refresh(instance)
return instance
def update(self, instance, data):
for key, value in data.items():
setattr(instance, key, value)
self.db.commit()
self.db.refresh(instance)
return instance
def delete(self, instance):
self.db.delete(instance)
self.db.commit()
return True
測試這個基本存儲庫:
# tests/repositories/test_base_repository.py
import pytest
from app.repositories.base_repository import BaseRepository
from app.models import User
@pytest.fixture
def base_repo(db):
return BaseRepository(db)
def test_create_and_get_by_id(base_repo):
# 創建用戶
user_data = {
"username": "baseuser",
"email": "base@example.com",
"hashed_password": "hashedpw"
}
user = base_repo.create(User, user_data)
# 獲取用戶
retrieved = base_repo.get_by_id(User, user.id)
# 驗證
assert retrieved.id == user.id
assert retrieved.username == user_data["username"]
assert retrieved.email == user_data["email"]
def test_get_all(base_repo):
# 創建多個用戶
users_data = [
{"username": "user1", "email": "user1@example.com", "hashed_password": "pw1"},
{"username": "user2", "email": "user2@example.com", "hashed_password": "pw2"},
{"username": "user3", "email": "user3@example.com", "hashed_password": "pw3"}
]
for data in users_data:
base_repo.create(User, data)
# 獲取所有用戶
users = base_repo.get_all(User)
# 驗證
assert len(users) >= 3
usernames = [user.username for user in users]
assert "user1" in usernames
assert "user2" in usernames
assert "user3" in usernames
def test_update(base_repo):
# 創建用戶
user_data = {
"username": "updateuser",
"email": "update@example.com",
"hashed_password": "hashedpw"
}
user = base_repo.create(User, user_data)
# 更新用戶
updated = base_repo.update(user, {"username": "updated"})
# 驗證
assert updated.id == user.id
assert updated.username == "updated"
assert updated.email == user_data["email"] # 未更新的字段保持不變
def test_delete(base_repo):
# 創建用戶
user_data = {
"username": "deleteuser",
"email": "delete@example.com",
"hashed_password": "hashedpw"
}
user = base_repo.create(User, user_data)
# 刪除用戶
result = base_repo.delete(user)
# 驗證
assert result is True
assert base_repo.get_by_id(User, user.id) is None
總結
數據庫測試是確保應用程序數據層正確性和性能的關鍵。通過測試數據庫模型、關係、存儲庫和查詢性能,你可以確保應用程序的數據訪問層正確工作。
方面 | 關鍵點 |
---|---|
測試環境 | 使用專用測試數據庫或內存數據庫 確保測試之間的隔離 |
測試數據 | 使用夾具或工廠模式創建測試數據 確保數據的一致性和可重複性 |
模型測試 | 測試模型關係和約束 確保模型行為符合預期 |
存儲庫測試 | 測試 CRUD 操作和複雜查詢 確保數據訪問層正確工作 |
性能測試 | 測試查詢性能 識別並優化慢查詢 |
通過全面的數據庫測試,你可以確保應用程序的數據層穩定可靠,為整個應用程序提供堅實的基礎。