Skip to content

测试策略:单元测试、集成测试、端到端测试 ​

测试金字塔 ​

         /\
        /E2E\      少量端到端测试
       /------\
      /集成测试\    中等数量集成测试
     /----------\
    /  单元测试  \   大量单元测试
   /--------------\

单元测试 ​

测试 Pydantic 模型 ​

python
from pydantic import ValidationError
import pytest

def test_user_create_valid():
    user = UserCreate(
        email="test@example.com",
        username="testuser",
        password="password123"
    )
    assert user.email == "test@example.com"

def test_user_create_invalid_email():
    with pytest.raises(ValidationError):
        UserCreate(
            email="invalid-email",
            username="testuser",
            password="password123"
        )

测试业务逻辑 ​

python
def test_password_hashing():
    password = "secret123"
    hashed = get_password_hash(password)
    
    assert hashed != password
    assert verify_password(password, hashed)
    assert not verify_password("wrong", hashed)

集成测试 ​

使用 TestClient ​

python
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_create_user():
    response = client.post(
        "/users/",
        json={
            "email": "test@example.com",
            "username": "testuser",
            "password": "password123"
        }
    )
    assert response.status_code == 201
    data = response.json()
    assert data["email"] == "test@example.com"
    assert "id" in data

def test_create_user_duplicate_email():
    # 第一次创建
    client.post("/users/", json={
        "email": "duplicate@example.com",
        "username": "user1",
        "password": "password123"
    })
    
    # 第二次创建(重复邮箱)
    response = client.post("/users/", json={
        "email": "duplicate@example.com",
        "username": "user2",
        "password": "password123"
    })
    assert response.status_code == 400
    assert "already registered" in response.json()["detail"]

测试数据库 ​

使用测试数据库:

python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from database import Base, get_db

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"

engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base.metadata.create_all(bind=engine)

def override_get_db():
    try:
        db = TestingSessionLocal()
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db

@pytest.fixture(scope="function")
def db():
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)
    yield

测试认证 ​

python
def test_login_success():
    # 先注册用户
    client.post("/users/", json={
        "email": "login@example.com",
        "username": "loginuser",
        "password": "password123"
    })
    
    # 登录
    response = client.post(
        "/token",
        data={"username": "loginuser", "password": "password123"}
    )
    assert response.status_code == 200
    data = response.json()
    assert "access_token" in data
    assert data["token_type"] == "bearer"

def test_access_protected_route():
    # 获取 token
    login_response = client.post(
        "/token",
        data={"username": "loginuser", "password": "password123"}
    )
    token = login_response.json()["access_token"]
    
    # 访问受保护路由
    response = client.get(
        "/users/me",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200
    assert response.json()["username"] == "loginuser"

pytest 配置 ​

conftest.py:

python
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from main import app
from database import Base, get_db

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"

engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

@pytest.fixture(scope="function")
def db_session():
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)
    
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

@pytest.fixture(scope="function")
def client(db_session):
    def override_get_db():
        try:
            yield db_session
        finally:
            pass
    
    app.dependency_overrides[get_db] = override_get_db
    yield TestClient(app)
    app.dependency_overrides.clear()

覆盖率 ​

bash
pip install pytest-cov

# 运行测试并生成覆盖率报告
pytest --cov=. --cov-report=html

# 查看报告
open htmlcov/index.html

返回第四章概览

用心记录每一步成长