前言
到目前为止,项目有 91 条用例,覆盖了单接口测试、链路测试、数据一致性、Session 验证、分页、边界值。这些都是"在已知条件下验证正确性"。
本篇进入更实际的领域,解决日常测试工作中会遇到的四个问题:
- 鉴权方式对比:MallLite 用 Session+Cookie,但实际项目更多用 JWT Token,区别是什么?测试上有什么不同?
- 超时和重试:第 10 篇在 ApiClient 里实现了重试机制,但没有用例验证它。网络抖动时框架真的能自动重试吗?
- 并发场景:两个人同时加同一件商品到购物车,库存会不会算错?
- 数据清理策略:用例之间数据互相污染,怎么系统化地解决?
另外有两个不在 MallLite 上跑、但面试和工作中会遇到的主题:Mock 外部接口、接口签名加密,作为理论+独立示例讲解。
一、鉴权方式对比
1.1 三种主流鉴权方式
方式 1:Session + Cookie(MallLite 使用)
登录 → 服务端创建 session,把 session_id 写到 Cookie
后续请求 → 浏览器自动带 Cookie → 服务端认出你是谁
requests 中 → Session 对象自动管理
方式 2:JWT Token(最常见的 API 鉴权方式)
登录 → 服务端返回一个 Token 字符串
后续请求 → 客户端在 Header 里带 Authorization: Bearer <token>
requests 中 → 每次手动设置 Header
方式 3:OAuth 2.0(第三方登录)
用户通过微信/Google 授权 → 拿到 access_token
access_token 会过期 → 用 refresh_token 刷新
1.2 测试上的区别
知识点:为什么鉴权方式影响测试代码?
不同鉴权方式决定了你的 ApiClient 怎么管理认证信息。用错了方式,所有需要登录的接口都会返回"未授权"。
| 维度 | Session+Cookie | JWT Token |
|---|---|---|
| 登录后存什么 | Cookie(自动存在 session 里) | Token 字符串(需要手动存) |
| 后续请求怎么带 | Session 自动带 Cookie | 每次手动设 Header |
| 自动管理 | requests.Session 自动处理 | 需要自己写 Token 刷新逻辑 |
| 多用户隔离 | 不同 Session 对象 | 不同 Token 字符串 |
1.3 在代码上的差异
python
# ===== Session + Cookie 方式(MallLite 使用)=====
# 登录后 session 自动保存 Cookie,后续请求自动携带
session = requests.Session()
session.post(url, json={"username": "admin", "password": "123"})
# 之后不用管,session 自动带 Cookie
session.get(url)
# ===== JWT Token 方式(假设 MallLite 用 Token)=====
# 登录后需要手动保存 Token,每次请求手动带 Header
session = requests.Session()
resp = session.post(url, json={"username": "admin", "password": "123"})
token = resp.json()["data"]["token"]
# 手动设置 Authorization header
session.headers.update({"Authorization": f"Bearer {token}"})
# 之后每次请求都会带这个 Header
session.get(url)
1.4 如果 MallLite 用 JWT Token,ApiClient 怎么改
知识点:为什么需要了解 Token 方式?
实际面试和工作中,大部分项目用 JWT Token。了解两种方式的代码差异,能帮你在不同项目间快速切换。
python
"""
如果 MallLite 用 JWT Token,ApiClient 需要加一个 login 方法
这个代码不在 MallLite 上运行,只做教学演示
"""
import requests
class TokenBasedApiClient:
"""JWT Token 方式的 ApiClient(教学演示)"""
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.Session()
self.token = None
def login(self, username, password):
"""
登录并保存 Token
跟 Session 方式的区别:
Session 方式:session 自动保存 Cookie,不用管
Token 方式:需要手动保存 token 字符串,手动设 header
"""
resp = self.session.post(
f"{self.base_url}/api/login",
json={"username": username, "password": password},
)
data = resp.json()
if data["code"] == 200:
self.token = data["data"]["token"]
# 手动设置后续请求的 Authorization header
self.session.headers.update({
"Authorization": f"Bearer {self.token}",
})
return resp
def get(self, path):
"""GET 请求(自动带 Token header)"""
return self.session.get(f"{self.base_url}{path}")
1.5 在 MallLite 上验证鉴权行为
【新建文件】 api/test_cases/test_auth.py:
python
"""
鉴权验证专题
MallLite 使用 Session + Cookie
这里专门验证不同鉴权状态下的接口行为
知识点:为什么需要测鉴权?
鉴权是安全的第一道防线。
如果未登录就能创建订单、删除数据,说明权限控制有漏洞。
即使 MallLite 是简化版系统,验证鉴权行为也能帮我们理解完整系统该怎么测。
"""
import pytest
from api_objects.user_api import UserApi
from api_objects.cart_api import CartApi
from api_objects.order_api import OrderApi
from common.assertions import ApiAssertions
class TestAuthComparison:
"""对比已登录和未登录的行为差异"""
@pytest.mark.regression
@pytest.mark.p1
def test_login_returns_session_info(self, user_api, assert_api):
"""
登录成功后返回用户信息
验证:登录响应包含 username 和 role
这些信息在前端显示用户名和控制菜单权限
"""
resp = user_api.login("admin", "admin123")
a = assert_api(resp)
a.assert_success()
a.assert_has_field("data.username")
a.assert_has_field("data.role")
@pytest.mark.regression
@pytest.mark.p1
def test_login_wrong_password_no_session(self, user_api, assert_api):
"""
登录失败后不应该有有效 session
验证:密码错误后用同一个 session 访问购物车
应该拿到空数据(未登录状态),而不是之前登录的数据
"""
resp = user_api.login("admin", "wrong_password")
assert_api(resp).assert_biz_fail()
# 用同一个 session(登录失败的)访问购物车
resp = user_api.get("/api/cart")
body = resp.json()
# 应该是空数据(未登录状态的默认购物车)
assert body["code"] == 200
assert body["data"]["item_count"] == 0
@pytest.mark.regression
@pytest.mark.p1
def test_login_then_logout_behavior(self, user_api, assert_api):
"""
登录 → 操作 → 用新 session(相当于"退出")→ 验证行为
已知行为:MallLite 的购物车是全局共享的,不按 session 隔离。
所以新 session 看到的购物车数据可能跟旧 session 一样。
这条用例记录这个行为,不硬断言隔离性。
"""
# 登录
user_api.login("admin", "admin123")
# 清空购物车后加商品
user_api.post("/api/cart/1") # 尝试删除
user_api.post("/api/cart", json_data={"product_id": 1, "quantity": 1})
# 验证购物车有数据
resp = user_api.get("/api/cart")
items = assert_api(resp).get_field("data.items")
assert len(items) > 0
# 创建新 session(相当于退出后重新打开浏览器)
new_user = UserApi("http://localhost:8000")
resp = new_user.get("/api/cart")
body = resp.json()
# 已知:购物车是全局共享的,新 session 也能看到数据
# 正式项目中这是一个安全漏洞
assert body["code"] == 200 # 只验证不崩溃
assert body["data"]["item_count"] >= 0 # 不断言是否隔离
new_user.close()
class TestTokenSimulation:
"""
模拟 JWT Token 方式的测试模式
知识点:MallLite 实际不用 JWT,但我们可以演示如果用 JWT 测试该怎么写。
这段代码不在 MallLite 上运行,只做教学。
"""
@pytest.mark.regression
def test_token_would_be_in_header(self):
"""
模拟:如果用 JWT Token,请求头应该包含 Authorization
这条用例演示的是测试思路,不是实际运行的代码。
如果你的项目用 JWT,按这个模式写就行。
"""
# 模拟 JWT Token 的使用方式
import requests
session = requests.Session()
# 假设登录返回 token
# resp = session.post(url, json={...})
# token = resp.json()["data"]["token"]
# 设置 token
fake_token = "eyJhbGciOiJIUzI1NiJ9.fake_token"
session.headers.update({"Authorization": f"Bearer {fake_token}"})
# 验证 header 中有 Authorization
assert "Authorization" in session.headers
assert session.headers["Authorization"] == f"Bearer {fake_token}"
session.close()
二、超时和重试验证
2.1 第 10 篇做了什么
第 10 篇在 base_api.py 中配置了自动重试:
python
retry_strategy = Retry(
total=2, # 最多重试 2 次
backoff_factor=0.5, # 退避间隔
status_forcelist=[500, 502, 503, 504], # 这些状态码触发重试
allowed_methods=["GET", "PUT", "DELETE"], # 只重试幂等请求
)
但一直没有用例验证它。怎么验证?用极端的 timeout 触发超时。
2.2 知识点:超时 vs 重试的区别
超时(Timeout):
发出请求后,等待 N 秒没有响应就算超时。
超时后抛 Timeout 异常。
解决的是"服务端卡住了"的问题。
重试(Retry):
请求失败后,等一会儿再试一次。
适合网络抖动、服务端临时过载的情况。
解决的是"偶尔失败"的问题。
两者的结合:
设置 timeout=5s + retry=2
→ 如果 5 秒没响应,超时
→ 等 0.5 秒后重试
→ 如果又 5 秒没响应,再等 1 秒重试
→ 最终还是失败才报错
2.3 实战
【新建文件】 api/test_cases/test_resilience.py:
python
"""
超时和重试验证
验证第 10 篇在 ApiClient 中实现的重试机制和容错能力
"""
import pytest
import requests
from api_objects.base_api import ApiClient
from common.assertions import ApiAssertions
class TestTimeout:
"""超时验证"""
@pytest.mark.regression
@pytest.mark.p2
def test_request_timeout_error(self):
"""
极短超时触发 Timeout 异常
已知:在 Windows + localhost 环境下,timeout=0.001 可能不会触发超时,
因为 localhost 的 TCP 连接是内存级别的,速度极快。
在远程服务器或 CI/CD 环境中,这个测试会正常触发 Timeout。
如果当前环境没触发,标记为 xfail(已知平台限制)。
"""
session = requests.Session()
try:
with pytest.raises(requests.exceptions.Timeout):
session.get("http://localhost:8000/api/products", timeout=0.001)
except pytest.fail.Exception:
# DID NOT RAISE --- localhost 太快,timeout 没生效
pytest.xfail("Windows + localhost 环境下极短 timeout 不可靠")
finally:
session.close()
@pytest.mark.regression
@pytest.mark.p2
def test_normal_request_no_timeout(self, product_api, assert_api):
"""正常请求不超时"""
resp = product_api.get_products()
assert_api(resp).assert_success()
@pytest.mark.regression
@pytest.mark.p2
def test_timeout_via_base_api(self):
"""
通过 ApiClient 发送带超时的请求
同 test_request_timeout_error,localhost 上可能不触发
"""
api = ApiClient("http://localhost:8000")
try:
with pytest.raises(requests.exceptions.Timeout):
api.get("/api/products", timeout=0.001)
except pytest.fail.Exception:
pytest.xfail("Windows + localhost 环境下极短 timeout 不可靠")
finally:
api.close()
@pytest.mark.regression
@pytest.mark.p2
def test_connection_error(self):
"""
连接不存在的服务器触发 ConnectionError
日志显示 Retry 会重试 2 次,共等约 15 秒后才报错
这恰好证明重试机制在工作
"""
api = ApiClient("http://localhost:19999")
with pytest.raises(requests.exceptions.ConnectionError):
api.get("/api/products")
api.close()
@pytest.mark.regression
@pytest.mark.p2
def test_invalid_url_returns_error(self):
"""请求不存在的路径,返回 404"""
api = ApiClient("http://localhost:8000")
resp = api.get("/api/nonexistent_path_xyz")
assert resp.status_code in (404, 405, 500)
api.close()
class TestRetryStrategy:
"""
重试策略验证
第 10 篇配置了:GET/PUT/DELETE 对 5xx 自动重试 2 次
POST 不重试,避免重复创建资源
"""
@pytest.mark.regression
@pytest.mark.p2
def test_retry_config_is_applied(self):
"""
验证重试配置已生效
检查 session 的 adapter 中 Retry 的参数
这不是测试 MallLite,而是测试我们自己的框架配置
"""
api = ApiClient("http://localhost:8000")
adapter = api.session.get_adapter("http://localhost:8000")
retry = adapter.max_retries
assert retry.total == 2, f"重试次数期望 2,实际 {retry.total}"
assert 500 in retry.status_forcelist
assert 502 in retry.status_forcelist
assert 503 in retry.status_forcelist
assert 504 in retry.status_forcelist
api.close()
@pytest.mark.regression
@pytest.mark.p2
def test_post_not_in_retry_list(self):
"""
POST 不在重试列表中
POST 创建资源,重试会重复创建
"""
api = ApiClient("http://localhost:8000")
adapter = api.session.get_adapter("http://localhost:8000")
retry = adapter.max_retries
if retry.allowed_methods is not None:
assert "POST" not in retry.allowed_methods, \
"POST 不应该在重试列表中"
api.close()
@pytest.mark.regression
@pytest.mark.p2
def test_request_statistics(self):
"""
验证请求统计功能
_request_count 记录总请求数,_fail_count 记录失败数
"""
api = ApiClient("http://localhost:8000")
assert api._request_count == 0
assert api._fail_count == 0
# 3 次正常请求
api.get("/api/products")
api.get("/api/products")
api.get("/api/products")
assert api._request_count == 3
assert api._fail_count == 0
# 1 次失败请求(404)
api.get("/api/nonexistent")
assert api._request_count == 4
assert api._fail_count == 1
api.close()
@pytest.mark.regression
@pytest.mark.p2
def test_multiple_requests_stable(self):
"""
连续多次请求都正常
验证 session 稳定性
"""
api = ApiClient("http://localhost:8000")
for i in range(5):
resp = api.get("/api/products")
assert resp.status_code == 200, f"第 {i+1} 次请求失败"
assert api._request_count == 5
assert api._fail_count == 0
api.close()
class TestConcurrency:
"""
并发场景测试
用 threading 模拟多用户同时操作
"""
@pytest.mark.regression
@pytest.mark.p2
def test_concurrent_login(self):
"""
多个线程同时登录同一个账号
验证:不会因为并发登录导致错误
"""
import threading
base = "http://localhost:8000"
results = []
def login_account():
try:
user = ApiClient(base)
resp = user.post("/api/login", json_data={
"username": "admin", "password": "admin123"
})
results.append(("success", resp.json()["code"]))
user.close()
except Exception as e:
results.append(("error", str(e)))
threads = [threading.Thread(target=login_account) for _ in range(3)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=30)
assert len(results) == 3
for status, code in results:
assert status == "success", f"有线程报错:{results}"
assert code == 200
@pytest.mark.regression
@pytest.mark.p2
def test_concurrent_get_products(self):
"""
多个线程同时查询商品列表
验证:并发读取不会导致错误
"""
import threading
base = "http://localhost:8000"
results = []
def get_products():
try:
api = ApiClient(base)
resp = api.get("/api/products")
results.append(("success", resp.status_code))
api.close()
except Exception as e:
results.append(("error", str(e)))
threads = [threading.Thread(target=get_products) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=30)
assert len(results) == 5
for status, code in results:
assert status == "success", f"有线程报错:{results}"
assert code == 200
三、并发场景模拟
3.1 为什么要测并发
单线程测试只能验证"一个用户操作时没问题"。但真实场景中,多个用户可能同时操作同一份数据:
场景 1:两个人同时加同一件商品到购物车
场景 2:两个人同时下单最后一件库存商品
场景 3:管理员正在修改商品价格,用户同时查看
如果服务端没有做好并发控制,可能出现数据不一致。
3.2 知识点:并发测试能发现什么 Bug?
Bug 1:库存超卖
商品库存 1 件,两个人同时下单,都成功了 → 卖出了 2 件(实际只有 1 件)
Bug 2:购物车数量错误
两个人同时把商品数量从 2 改成 5 → 最终结果可能是 5,也可能是 2(取决于谁后写入)
Bug 3:订单号重复
两个人同时创建订单,生成了相同的订单号 → 后一个覆盖了前一个
3.3 实战
【追加到】 api/test_cases/test_resilience.py,在文件末尾追加:
python
# ===== 以下是并发测试 =====
import threading
import time
from api_objects.user_api import UserApi
from api_objects.cart_api import CartApi
from api_objects.order_api import OrderApi
class TestConcurrency:
"""
并发场景测试
用 Python 的 threading 模块模拟多用户同时操作
知识点:threading 的作用
每个线程模拟一个用户的操作。
多个线程同时启动,模拟"同一时刻"多个请求并发到达服务端。
"""
@pytest.mark.regression
@pytest.mark.p2
def test_concurrent_add_to_cart(self):
"""
两个线程同时加同一件商品到购物车
验证:两个请求都成功,购物车最终状态正确
"""
base = "http://localhost:8000"
results = []
def add_cart(username, password):
"""每个线程的执行函数"""
try:
user = UserApi(base)
user.login(username, password)
cart = CartApi(base)
cart.session = user.session
cart.clear_cart()
resp = cart.add_to_cart(product_id=1, quantity=1)
results.append(("success", resp.json()["code"]))
user.close()
except Exception as e:
results.append(("error", str(e)))
# 创建两个线程,同时启动
t1 = threading.Thread(target=add_cart, args=("admin", "admin123"))
t2 = threading.Thread(target=add_cart, args=("testuser", "test123"))
t1.start()
t2.start()
# 等待两个线程都完成
t1.join(timeout=30)
t2.join(timeout=30)
# 两个都应该成功
assert len(results) == 2, f"期望 2 个结果,实际 {len(results)}"
for status, code in results:
assert status == "success", f"有线程报错:{results}"
assert code == 200, f"有请求失败:code={code}"
@pytest.mark.regression
@pytest.mark.p2
def test_concurrent_login(self):
"""
多个线程同时登录同一个账号
验证:不会因为并发登录导致错误
"""
base = "http://localhost:8000"
results = []
def login_account():
try:
user = UserApi(base)
resp = user.login("admin", "admin123")
results.append(resp.json()["code"])
user.close()
except Exception as e:
results.append(str(e))
threads = []
for _ in range(3):
t = threading.Thread(target=login_account)
threads.append(t)
# 同时启动 3 个线程
for t in threads:
t.start()
# 等待全部完成
for t in threads:
t.join(timeout=30)
# 都应该成功
assert len(results) == 3
for code in results:
assert code == 200, f"并发登录失败:{results}"
@pytest.mark.regression
@pytest.mark.p2
def test_concurrent_create_order(self):
"""
两个线程同时创建订单
验证:不会因为并发下单导致错误
"""
base = "http://localhost:8000"
results = []
def create_order():
try:
user = UserApi(base)
user.login("admin", "admin123")
cart = CartApi(base)
cart.session = user.session
cart.clear_cart()
cart.add_to_cart(product_id=1, quantity=1)
order = OrderApi(base)
order.session = user.session
resp = order.create_order()
results.append(("success", resp.json()["code"]))
user.close()
except Exception as e:
results.append(("error", str(e)))
t1 = threading.Thread(target=create_order)
t2 = threading.Thread(target=create_order)
t1.start()
t2.start()
t1.join(timeout=30)
t2.join(timeout=30)
assert len(results) == 2
for status, code in results:
assert status == "success", f"有线程报错:{results}"
3.4 并发测试的局限性
知识点:threading 的局限
Python 的 GIL(全局解释器锁)限制了真正的多线程并行。threading 模块模拟的是"交替执行",不是"真正同时"。对于接口测试来说足够了,因为瓶颈在网络 I/O 而不是 CPU。
如果需要更高并发,可以用 Locust(第 17 篇会讲)或 asyncio。
四、数据清理策略
4.1 问题回顾
之前的 conftest.py 里用了 autouse=True 的 clean_cart fixture,每个用例执行前自动清空购物车。这只解决了购物车的问题。如果用例创建了订单呢?注册了用户呢?
4.2 四种清理策略
知识点:选择哪种策略取决于数据创建的成本和副作用。
| 策略 | 适用场景 | 实现方式 |
|---|---|---|
| 前置清理 | 每次操作前清空再开始 | autouse fixture 在 setup 清理 |
| 后置清理 | 操作后恢复原状 | yield fixture 在 teardown 清理 |
| 独立数据 | 每次创建唯一的数据 | 用时间戳做唯一标识 |
| 不清理 | 只读操作、不影响数据的测试 | 不需要 fixture |
4.3 各策略的实际代码
【追加到】 api/conftest.py,替换原有的 clean_cart fixture,并追加其他清理策略:
python
# ===== 数据清理策略 =====
@pytest.fixture(autouse=True)
def clean_cart(admin_session):
"""
策略 1:前置清理
autouse=True 表示自动对所有用例生效。
每个用例开始前自动清空购物车,避免上一个用例残留的数据影响当前用例。
适用场景:购物车这种会被频繁修改的数据
"""
cart = CartApi(BASE_URL)
cart.session = admin_session
cart.clear_cart()
yield # 用例执行
@pytest.fixture
def unique_username():
"""
策略 3:独立数据
用时间戳生成唯一用户名,每个用例注册不同的账号。
不需要清理,因为每个账号只用一次。
适用场景:注册接口测试
"""
return f"test_{int(time.time() * 1000)}"
# 以下是策略 2 的示例(不在 MallLite 上运行,做教学演示):
#
# @pytest.fixture
# def created_and_cleanup(admin_cart_api, admin_order_api):
# """
# 策略 2:后置清理
#
# yield 之前是 setup(创建数据)
# yield 之后是 teardown(清理数据)
#
# 适用场景:创建了不可逆的数据(如订单),需要在用例结束后清理
# """
# admin_cart_api.add_to_cart(product_id=1, quantity=1)
# resp = admin_order_api.create_order()
# order_no = resp.json()["data"]["order_no"]
#
# yield order_no # 用例执行,把 order_no 传给用例
#
# # 用例结束后清理(如果 MallLite 有删除订单的接口)
# # admin_order_api.delete_order(order_no)
4.4 什么时候用哪种
购物车 → 前置清理(autouse):每次用例前清空,简单直接
订单 → 独立数据或后置清理:订单号是唯一的,一般不冲突
用户注册 → 独立数据(时间戳):每个用例注册不同用户名,不冲突
商品 → 不清理:商品是只读的,不修改数据
五、Mock 外部接口(理论 + 独立示例)
5.1 什么是 Mock
当你的接口依赖第三方服务时(如支付、短信、物流),测试中不可能真的调用第三方。Mock 就是"假装"第三方返回了你期望的结果。
没有 Mock:
创建订单 → 调用支付宝接口 → 支付宝返回"支付成功" → 订单完成
问题:测试环境没有真实的支付宝接口
有了 Mock:
创建订单 → 拦截对支付宝的请求 → 直接返回"支付成功" → 订单完成
测试不依赖外部服务,稳定且快速
5.2 知识点:responses 库
responses 是 Python 中 mock HTTP 请求的标准库。它能拦截 requests 发出的请求,返回预设的响应。
powershell
pip install responses
5.3 独立示例
【新建文件】 api/examples/mock_demo.py:
python
"""
Mock 外部接口示例
这个文件不在 MallLite 上运行,是独立的教学示例。
演示如何用 responses 库 mock 第三方接口。
运行方式:python api/examples/mock_demo.py
"""
import requests
import responses
# ===== 场景:假设订单接口需要调用第三方支付 =====
def create_order_with_payment(order_data):
"""
创建订单并调用第三方支付接口
这个函数模拟的是:订单服务先调用支付接口验证支付状态,
如果支付成功才创建订单。
"""
# 第一步:调用支付接口
payment_resp = requests.post(
"https://pay.example.com/api/verify",
json={"order_id": order_data["order_id"], "amount": order_data["amount"]},
)
payment_result = payment_resp.json()
if payment_result["status"] == "success":
return {"code": 200, "message": "订单创建成功", "order_no": "ORD001"}
else:
return {"code": 400, "message": "支付失败"}
# ===== 没有 Mock 的情况 =====
# 调用会真的去请求 https://pay.example.com,但这个地址不存在,会报错
# ===== 用 Mock 的情况 =====
@responses.activate
def test_create_order_with_mock_payment():
"""
用 responses.mock 拦截支付请求,返回预设结果
知识点:@responses.activate 装饰器
这个装饰器让 responses 开始拦截 HTTP 请求。
在函数内注册的 mock 规则,会在函数结束时自动清除。
"""
# 注册 mock 规则:如果有人 POST https://pay.example.com/api/verify
# 就返回 {"status": "success"},状态码 200
responses.add(
responses.POST,
"https://pay.example.com/api/verify",
json={"status": "success"},
status=200,
)
# 调用被测函数
result = create_order_with_payment({"order_id": "ORD001", "amount": 8999})
# 验证结果
assert result["code"] == 200
assert result["message"] == "订单创建成功"
print("✓ Mock 支付成功场景通过")
@responses.activate
def test_create_order_with_failed_payment():
"""模拟支付失败的场景"""
responses.add(
responses.POST,
"https://pay.example.com/api/verify",
json={"status": "failed", "reason": "余额不足"},
status=200,
)
result = create_order_with_payment({"order_id": "ORD002", "amount": 99999})
assert result["code"] == 400
assert result["message"] == "支付失败"
print("✓ Mock 支付失败场景通过")
if __name__ == "__main__":
test_create_order_with_mock_payment()
test_create_order_with_failed_payment()
print("\n所有 Mock 示例通过")
运行:
powershell
python api\examples\mock_demo.py
六、接口签名加密(理论 + 独立示例)
6.1 什么是接口签名
有些 API 需要对请求参数做签名,防止被篡改。签名的流程:
1. 把所有参数按字母排序拼接:amount=100&order_id=ORD001&user_id=1
2. 加上密钥拼接:amount=100&order_id=ORD001&user_id=1&secret_key=mykey
3. 对拼接后的字符串做 MD5/SHA256:hash = md5(拼接串)
4. 把 hash 作为 sign 参数加到请求中
服务端用同样的方式计算签名,对比是否一致。如果参数被中间人篡改了,签名就对不上。
6.2 知识点:怎么在测试中处理签名
python
"""
接口签名示例
这个文件不在 MallLite 上运行,是独立的教学示例。
演示如何在测试中生成正确的签名。
运行方式:python api/examples/sign_demo.py
"""
import hashlib
import json
def generate_sign(params, secret_key):
"""
生成签名
签名算法:
1. 把参数按 key 字母排序
2. 拼接成 key=value&key=value 的格式
3. 末尾加上 &secret_key=xxx
4. 对整个字符串做 MD5
"""
# 按 key 排序
sorted_keys = sorted(params.keys())
# 拼接
sign_str = "&".join(f"{k}={params[k]}" for k in sorted_keys)
sign_str += f"&secret_key={secret_key}"
# MD5
sign = hashlib.md5(sign_str.encode("utf-8")).hexdigest()
return sign
# 演示
if __name__ == "__main__":
# 假设的请求参数
params = {
"order_id": "ORD001",
"amount": "100",
"user_id": "1",
}
secret_key = "my_secret_key"
sign = generate_sign(params, secret_key)
print(f"参数:{json.dumps(params, indent=2)}")
print(f"密钥:{secret_key}")
print(f"签名:{sign}")
# 验证:篡改参数后签名不同
tampered_params = params.copy()
tampered_params["amount"] = "99999" # 篡改金额
tampered_sign = generate_sign(tampered_params, secret_key)
print(f"\n篡改后签名:{tampered_sign}")
print(f"签名不同:{sign != tampered_sign}") # 应该是 True
# 在测试中的用法:
# resp = api.post("/api/orders", json_data={
# "order_id": "ORD001",
# "amount": "100",
# "user_id": "1",
# "sign": generate_sign(params, secret_key),
# })
运行:
powershell
python api\examples\sign_demo.py
七、运行验证
powershell
pytest -v
预期用例分布:
| 文件 | 用例数 | 来源 |
|---|---|---|
| test_user.py | 15 | 第 10 篇 |
| test_product.py | 12 | 第 10 篇 |
| test_cart.py | 9 | 第 9 篇 |
| test_order.py | 8 | 第 9 篇 |
| test_shopping_flow.py | 7 | 第 11 篇 |
| test_extractor_practice.py | 5 | 第 11 篇 |
| test_data_consistency.py | 9 | 第 11 篇 |
| test_session.py | 7 | 第 11 篇 |
| test_pagination.py | 8 | 第 11 篇 |
| test_edge_cases.py | 11 | 第 11 篇 |
| test_auth.py | 5 | 本篇 |
| test_resilience.py | 10 | 本篇 |
| 合计 | 106 |
powershell
# 只跑鉴权测试
pytest test_cases/test_auth.py -v
# 只跑超时和并发
pytest test_cases/test_resilience.py -v
# 跑 Mock 示例(独立脚本)
python api\examples\mock_demo.py
# 跑签名示例(独立脚本)
python api\examples\sign_demo.py
八、本篇新增和修改的文件
| 文件 | 操作 | 说明 |
|---|---|---|
test_cases/test_auth.py |
新建 | 5 条鉴权验证用例 |
test_cases/test_resilience.py |
新建 | 7 条超时重试 + 3 条并发用例 |
examples/mock_demo.py |
新建 | Mock 外部接口独立示例 |
examples/sign_demo.py |
新建 | 接口签名加密独立示例 |
conftest.py |
修改 | 完善数据清理策略注释 |
requirements.txt |
修改 | 新增 responses 依赖 |
九、今日成果
- 讲解了三种鉴权方式(Session+Cookie、JWT Token、OAuth)的区别和测试差异
- 编写了鉴权验证用例(登录信息验证、失败后无 session、模拟退出)
- 编写了超时和重试验证(极端超时触发、连接错误、重试配置检查、请求统计)
- 编写了并发场景用例(并发加购、并发登录、并发下单)
- 讲解了四种数据清理策略(前置清理、后置清理、独立数据、不清理)
- 讲解了 Mock 外部接口的原理和 responses 库用法(独立示例)
- 讲解了接口签名加密的原理和实现(独立示例)
- 项目累计 106 条用例
十、下篇预告
13 - 接口数据驱动与报告
最后一篇:YAML 数据驱动、Allure 报告集成(请求/响应自动附加到报告中、用例分类、严重级别、Step 装饰器)、接口自动化篇完整项目总结。
接口自动化篇进度:12/13。