#Python 测试实战案例
预计阅读时间: 11 分钟本文提供 3 个完整的 Python 自动化测试实战案例,每个案例包含从需求描述、提示词、完整代码到运行验证的全流程。案例覆盖 Web UI E2E、REST API CRUD 和 UI+API 混合测试三种最经典的测试场景。
Info
本文是 Python 测试工具链概览 的实战补充。PO 模式详解请参考 Page Object Model 深度实践,API 测试架构请参考 API 测试架构模式。
#场景一:电商 Web UI 端到端测试
#业务流程
首页 → 搜索"无线耳机"→ 点击第一个商品 → 进入详情页
→ 选择数量为 2 → 加入购物车 → 进入购物车 → 验证商品和数量
→ 点击结算 → 进入结算页 → 填写收货地址 → 提交订单 → 验证成功提示#项目结构
ecommerce-tests/
├── conftest.py # 共享 fixture
├── pages/
│ ├── base_page.py
│ ├── home_page.py
│ ├── product_detail_page.py
│ ├── cart_page.py
│ └── checkout_page.py
├── pages/components/
│ └── header.py
├── data/
│ └── users.py # 测试用户数据
└── tests/
├── conftest.py
└── test_e2e_checkout.py#Claude Code 提示词
> 为电商网站编写 Playwright + pytest 端到端测试:
>
> 测试流程:首页 → 搜索"无线耳机" → 点击第一个商品 → 数量选 2 → 加入购物车
> → 进入购物车验证商品和数量 → 结算 → 填写地址 → 提交订单 → 验证成功
>
> 要求:
> - 使用 Page Object Model,每个页面一个类
> - BasePage 封装 navigate()、wait_for_loaded()、screenshot()
> - 共享组件(Header)提取为独立类
> - conftest.py 管理 fixture 层级
> - 测试数据(用户、地址)放在 data/ 目录
> - 每个关键步骤添加截图
> - 使用 data-testid 作为首选选择器#完整代码
conftest.py(项目根):
import pytest
from playwright.sync_api import Page, BrowserContext
BASE_URL = "http://localhost:3000"
@pytest.fixture(scope="session")
def browser_context(browser: "Browser") -> "BrowserContext":
"""session 级别,共享浏览器上下文"""
context = browser.new_context(
viewport={"width": 1440, "height": 900},
locale="zh-CN",
)
yield context
context.close()
@pytest.fixture(scope="function")
def page(browser_context: BrowserContext) -> Page:
"""每个测试独立的新页面"""
page = browser_context.new_page()
yield page
page.close()
@pytest.fixture(scope="function")
def home_page(page: Page) -> "HomePage":
"""导航到首页并等待加载完成"""
from pages.home_page import HomePage
hp = HomePage(page, BASE_URL)
hp.navigate().wait_for_loaded()
return hp
@pytest.fixture(scope="function")
def logged_in_home_page(page: Page) -> "HomePage":
"""已登录用户的首页"""
from pages.home_page import HomePage
from pages.login_page import LoginPage
from data.users import TEST_USER
hp = HomePage(page, BASE_URL)
hp.navigate()
hp.header.go_to_login()
lp = LoginPage(page, BASE_URL)
lp.login(TEST_USER["username"], TEST_USER["password"])
return hptests/test_e2e_checkout.py:
from playwright.sync_api import Page, expect
from pages.home_page import HomePage
from pages.product_detail_page import ProductDetailPage
from pages.cart_page import CartPage
from pages.checkout_page import CheckoutPage
def test_complete_checkout_flow(logged_in_home_page: HomePage):
"""
完整购物流程 E2E 测试
首页 → 搜索 → 商品详情 → 加购 → 购物车 → 结算 → 提交订单
"""
hp = logged_in_home_page
# === Step 1: 搜索商品 ===
hp.header.search("无线耳机")
hp.screenshot("01-search-results")
# 验证搜索结果
assert len(hp.get_all_product_names()) > 0
first_product_name = hp.get_first_product_name()
# === Step 2: 进入商品详情页 ===
pdp = hp.click_first_product()
pdp.wait_for_loaded()
pdp.screenshot("02-product-detail")
# 验证详情页
assert pdp.get_product_name() == first_product_name
assert pdp.get_price_value() > 0
# === Step 3: 选择数量并加入购物车 ===
pdp.select_quantity(2)
pdp.add_to_cart()
pdp.screenshot("03-added-to-cart")
# === Step 4: 进入购物车验证 ===
cart = CartPage(hp.page, hp.base_url)
cart.navigate().wait_for_loaded()
cart.screenshot("04-cart")
cart_items = cart.get_item_names()
assert first_product_name in cart_items
assert cart.get_item_quantity(first_product_name) == 2
# === Step 5: 进入结算页 ===
checkout = cart.proceed_to_checkout()
checkout.screenshot("05-checkout")
# === Step 6: 填写收货地址 ===
from data.users import SHIPPING_ADDRESS
checkout.fill_shipping_address(SHIPPING_ADDRESS)
checkout.screenshot("06-address-filled")
# === Step 7: 提交订单 ===
checkout.select_payment_method("credit_card")
order_confirmation = checkout.submit_order()
order_confirmation.screenshot("07-order-confirmed")
# === Step 8: 验证订单成功 ===
expect(order_confirmation.success_icon).to_be_visible()
expect(order_confirmation.order_number).to_be_visible()
assert order_confirmation.get_order_status() == "已下单"#运行与调试
# 带界面运行(可视化调试)
pytest tests/test_e2e_checkout.py -s --headed --slowmo 1000
# 开启 Trace(失败时自动保存)
pytest tests/test_e2e_checkout.py --tracing on
# 查看失败 Trace
playwright show-trace test-results/.../trace.zip
# 并行运行所有 UI 测试
pytest tests/ -n auto --headed#场景二:REST API CRUD 全流程
#测试目标
以用户管理 API 为例,完整测试 CRUD 操作链:
POST /api/auth/register → 创建用户
GET /api/users/{id} → 查询用户
PUT /api/users/{id} → 更新用户
GET /api/users/{id} → 验证更新
POST /api/auth/login → 登录验证
DELETE /api/users/{id} → 删除用户
GET /api/users/{id} → 验证删除(404)#Claude Code 提示词
> 为 REST API 用户管理模块编写 pytest + httpx 接口测试:
>
> 测试 CRUD 完整流程:注册 → 查询 → 更新 → 验证更新 → 登录 → 删除 → 验证删除
>
> 要求:
> - 使用 pytest-asyncio + httpx.AsyncClient
> - base_url 通过 fixture 注入,支持环境变量覆盖
> - pydantic 模型校验响应结构
> - conftest.py 中 session scope fixture 管理 admin token
> - 每一个 API 调用断言状态码 + 关键字段 + 结构校验
> - 使用 parametrize 覆盖字段校验的边界值
> - 数据驱动测试覆盖创建用户的异常输入场景#完整代码
conftest.py:
import pytest
import os
from httpx import AsyncClient
@pytest.fixture(scope="session")
def base_url():
return os.getenv("API_BASE_URL", "http://localhost:8000")
@pytest.fixture(scope="session")
async def admin_token(base_url: str):
"""管理员 token——整个 session 只登录一次"""
async with AsyncClient(base_url=base_url) as client:
resp = await client.post("/api/auth/login", json={
"username": "admin",
"password": os.getenv("ADMIN_PASSWORD", "admin123"),
})
assert resp.status_code == 200
return resp.json()["access_token"]
@pytest.fixture
async def client(base_url: str):
"""匿名客户端"""
async with AsyncClient(base_url=base_url, timeout=30.0) as c:
yield c
@pytest.fixture
async def auth_client(base_url: str, admin_token: str):
"""管理员客户端"""
async with AsyncClient(
base_url=base_url,
timeout=30.0,
headers={"Authorization": f"Bearer {admin_token}"},
) as c:
yield c
@pytest.fixture
async def created_user(auth_client: AsyncClient):
"""创建测试用户并在测试结束后自动删除"""
import uuid
unique = uuid.uuid4().hex[:8]
payload = {
"username": f"test_user_{unique}",
"email": f"test_{unique}@example.com",
"password": "TestPass123!",
"full_name": "Test User",
}
resp = await auth_client.post("/api/auth/register", json=payload)
assert resp.status_code == 201
user = resp.json()
yield user
# 清理
await auth_client.delete(f"/api/users/{user['id']}")tests/api/test_user_crud.py:
import pytest
from httpx import AsyncClient
from schemas.user import UserResponse
@pytest.mark.asyncio
class TestUserCRUD:
"""用户管理 CRUD 测试"""
async def test_01_register_user(self, auth_client: AsyncClient):
"""POST /api/auth/register — 创建用户"""
import uuid
unique = uuid.uuid4().hex[:8]
payload = {
"username": f"crud_test_{unique}",
"email": f"crud_{unique}@example.com",
"password": "TestPass123!",
"full_name": "CRUD Test User",
}
resp = await auth_client.post("/api/auth/register", json=payload)
assert resp.status_code == 201
user = UserResponse(**resp.json())
assert user.username == payload["username"]
assert user.email == payload["email"]
assert "password" not in resp.json()
# 存储供后续测试使用
TestUserCRUD.test_user_id = user.id
async def test_02_get_user(self, auth_client: AsyncClient):
"""GET /api/users/{id} — 查询用户"""
resp = await auth_client.get(
f"/api/users/{TestUserCRUD.test_user_id}"
)
assert resp.status_code == 200
user = UserResponse(**resp.json())
assert user.id == TestUserCRUD.test_user_id
assert user.is_active is True
async def test_03_update_user(self, auth_client: AsyncClient):
"""PUT /api/users/{id} — 更新用户"""
payload = {"full_name": "Updated Name", "bio": "New bio"}
resp = await auth_client.put(
f"/api/users/{TestUserCRUD.test_user_id}", json=payload
)
assert resp.status_code == 200
user = resp.json()
assert user["full_name"] == "Updated Name"
assert user["bio"] == "New bio"
async def test_04_verify_update(self, auth_client: AsyncClient):
"""GET /api/users/{id} — 验证更新持久化"""
resp = await auth_client.get(
f"/api/users/{TestUserCRUD.test_user_id}"
)
assert resp.status_code == 200
assert resp.json()["full_name"] == "Updated Name"
async def test_05_delete_user(self, auth_client: AsyncClient):
"""DELETE /api/users/{id} — 删除用户"""
resp = await auth_client.delete(
f"/api/users/{TestUserCRUD.test_user_id}"
)
assert resp.status_code == 204
async def test_06_verify_deleted(self, auth_client: AsyncClient):
"""GET /api/users/{id} — 验证删除后返回 404"""
resp = await auth_client.get(
f"/api/users/{TestUserCRUD.test_user_id}"
)
assert resp.status_code == 404#JSON Schema 响应验证
# schemas/user.py
from pydantic import BaseModel, Field
from datetime import datetime
class UserResponse(BaseModel):
id: int
username: str = Field(min_length=3, max_length=50)
email: str
full_name: str | None = None
bio: str | None = None
is_active: bool
role: str
created_at: datetime
updated_at: datetime
class UserListResponse(BaseModel):
items: list[UserResponse]
total: int
page: int
page_size: int
total_pages: int#场景三:UI + API 混合测试
#测试思路
API 创建测试数据 → UI 验证页面展示 → API 清理数据这种模式避免了纯 UI 测试对测试数据的依赖——测试数据由 API 精确控制,UI 只负责验证展示效果。
#Claude Code 提示词
> 编写 Python 混合测试,API + UI 协作:
>
> 流程:
> 1. 先用 httpx 调用 POST /api/products 创建 3 个测试商品(名称含品牌标识便于识别)
> 2. 再用 Playwright 打开商品列表页,验证这 3 个商品正确显示(名称、价格)
> 3. 点击第一个商品进入详情页,验证详情页数据与 API 返回一致
> 4. 测试完成后用 httpx DELETE 清理测试数据
>
> 要求:
> - API 和 UI 部分使用独立的 fixture
> - 共用一个声明了 scope='module' 的 fixture 传递测试数据
> - UI 部分使用 Page Object Model#完整代码
# tests/test_hybrid_product.py
import pytest
from httpx import AsyncClient
from playwright.sync_api import Page
from pages.product_list_page import ProductListPage
from pages.product_detail_page import ProductDetailPage
@pytest.fixture(scope="module")
def test_products():
"""module 级别——本模块所有测试共享的测试数据"""
return [
{
"name": "混合测试-商品A",
"price": 99.00,
"description": "API 创建的商品 A",
},
{
"name": "混合测试-商品B",
"price": 199.00,
"description": "API 创建的商品 B",
},
{
"name": "混合测试-商品C",
"price": 299.00,
"description": "API 创建的商品 C",
},
]
@pytest.fixture(scope="module")
async def api_created_products(
auth_client: AsyncClient, test_products: list[dict]
):
"""通过 API 创建测试商品,测试结束后清理"""
created = []
for product in test_products:
resp = await auth_client.post("/api/products", json=product)
assert resp.status_code == 201
created.append(resp.json())
yield created
# 清理所有创建的商品
for product in created:
await auth_client.delete(f"/api/products/{product['id']}")
def test_ui_displays_api_created_products(
page: Page,
api_created_products: list[dict],
test_products: list[dict],
):
"""UI 验证:API 创建的商品正确显示在列表页"""
# Given: 3 个商品已通过 API 创建
plp = ProductListPage(page, "http://localhost:3000")
plp.navigate().wait_for_loaded()
# When: 获取页面上的商品名称
displayed_names = plp.get_all_product_names()
# Then: 所有 API 创建的商品都显示在页面上
for product in test_products:
assert product["name"] in displayed_names, (
f"商品 '{product['name']}' 未在列表页显示"
)
def test_ui_product_detail_matches_api_data(
page: Page,
api_created_products: list[dict],
):
"""UI 验证:商品详情页的数据与 API 一致"""
# Given: API 创建的第一个商品
api_product = api_created_products[0]
# When: UI 打开该商品的详情页
pdp = ProductDetailPage(page, "http://localhost:3000")
pdp.navigate_to_product(api_product["id"]).wait_for_loaded()
# Then: UI 显示的数据与 API 数据一致
assert pdp.get_product_name() == api_product["name"]
assert pdp.get_price_value() == api_product["price"]
assert pdp.get_description() == api_product["description"]Tip
UI + API 混合测试的核心优势在于数据可控性。纯 UI 测试依赖数据库中已存在的数据(不可控),纯 API 测试不验证前端展示(不全面)。混合模式是两者的最佳平衡:API 精确创建数据,UI 验证展示,API 清理收尾。

