功能可用性自动化测试验证
一、为什么需要"可用性"这一层测试
单元测试验证函数逻辑,集成测试验证模块协作,但线上故障往往出在两者之外:配置错误、依赖服务超时、证书过期、限流熔断。这类问题不会让任何单测变红,却会让功能整体不可用。可用性测试(Availability / Smoke Testing)要回答的只有三个问题:服务是否活着、核心链路是否通、失败后能否自动发现。
它必须满足三个工程属性:可重复、低成本、可集成到发布流程。下面给出一套可落地的实现。
二、分层设计
| 层级 | 检测对象 | 判定方式 | 频率 |
|---|---|---|---|
| L1 存活探测 | 进程 / 端口 | TCP 或 /healthz 返回 200 |
30s |
| L2 依赖探测 | DB、Redis、下游 API | 轻量读写、连通性 | 1min |
| L3 核心链路 | 登录→查询→下单等关键路径 | 端到端断言业务字段 | 每次发布 + 5min |
分层的关键是快:L1/L2 单次执行控制在百毫秒级,L3 允许秒级但必须串行可重放。
三、探活与重试的实现
网络抖动不应直接判死,但重试必须有上限和退避,否则探测本身会放大故障。
python
import time
import requests
def probe(url: str, timeout: float = 3.0, retries: int = 3, backoff: float = 0.5) -> dict:
"""返回结构化探测结果,便于聚合统计与告警。"""
last_err = None
for i in range(retries):
start = time.perf_counter()
try:
resp = requests.get(url, timeout=timeout)
cost = (time.perf_counter() - start) * 1000
if resp.status_code == 200 and resp.json().get("status") == "ok":
return {"ok": True, "attempt": i + 1, "cost_ms": round(cost, 1)}
last_err = f"HTTP {resp.status_code}"
except Exception as e:
last_err = repr(e)
if i < retries - 1:
time.sleep(backoff * (2 ** i)) # 指数退避
return {"ok": False, "attempt": retries, "error": last_err}
注意两点:健康检查接口不要做重业务查询,否则探测会变成压测;返回体必须带语义字段,只判断 200 无法区分"进程活着但依赖全挂"。
四、依赖与核心链路的断言
依赖探测要"读一点、写一点、删掉",避免污染数据。
python
def check_redis(conn, key="__probe__"):
conn.set(key, "1", ex=5)
assert conn.get(key) == b"1"
conn.delete(key)
def check_core_flow(client):
token = client.post("/api/login", json={"u": "probe", "p": "probe"}).json()["token"]
headers = {"Authorization": f"Bearer {token}"}
r = client.get("/api/orders?limit=1", headers=headers)
assert r.status_code == 200
body = r.json()
assert "items" in body and body["total"] >= 0 # 断言业务字段,而非仅状态码
核心链路断言的常见误区是只判 status_code。业务可用性问题常表现为 200 但字段缺失、返回空列表、金额为 0。断言业务不变量(invariant)才有意义。
五、用 pytest 组织并接入 CI
python
import pytest, json
CASES = json.load(open("availability_cases.json")) # 环境、URL、期望字段外置
@pytest.mark.availability
@pytest.mark.parametrize("case", CASES, ids=[c["name"] for c in CASES])
def test_availability(case):
result = probe(case["url"])
assert result["ok"], f"{case['name']} 不可用: {result.get('error')}"
配合 CI,把可用性测试作为发布流水线的门禁,失败即阻断发布并输出结构化报告:
yaml
- name: Availability Gate
run: pytest -m availability --junitxml=report/availability.xml --timeout=60
- name: Notify
if: failure()
run: ./scripts/alert.sh "$(date -u +%FT%TZ) availability gate failed"
六、结果判定与告警策略
可用性测试的价值在噪声控制,建议遵守三条规则:
- 连续失败才告警(如连续 3 次),单次超时只记录不告警,避免抖动刷屏。
- 区分故障等级:L1 失败 = P0 立即告警;L3 失败 = P1 工单;L2 单依赖失败 = P2 日报聚合。
- 记录趋势而非只看当次 :采集
cost_ms与失败率,做周维度对比,性能劣化往往先于故障出现。
python
def judge(history, consecutive=3):
tail = history[-consecutive:]
if len(tail) == consecutive and all(not h["ok"] for h in tail):
return "P0", "连续失败,触发告警"
if history and not history[-1]["ok"]:
return "P1", "单次失败,记录待观察"
return "OK", "正常"
七、落地清单
- 健康检查接口与业务接口分离,禁止重查询;
- 探测数据可回滚、可清理,不侵入生产数据;
- 所有断言落到业务字段,不只判 HTTP 状态码;
- 外置用例配置,多环境同一套脚本;
- 接入 CI 门禁与定时调度,失败分等级告警;
- 留存执行耗时与失败率,用于趋势分析。
可用性测试不追求覆盖率,追求的是"故障发生后,系统比用户先知道"。把上面几层跑稳,发布流程就具备了最基本的自证能力。
海量精选技术文档和实战案例持续更新,敬请关注【风骏时光少年】