SerpBase 采集脚本的时间注入:让测试可复现
一个只在晚上挂的测试
有个测试白天一直通过,晚上跑 CI 就红。查了半天,原因是代码里直接调了 datetime.now(),测试断言的是「今天」的日期——跨过零点,断言就失效了。
这类问题很隐蔽:逻辑本身没错,错的是时间成了隐藏依赖。采集脚本尤其容易踩,因为几乎每个环节都要记时间:采集时刻、重试间隔、日切、缓存过期。
把时间变成参数
最直接的改法:函数需要时间,就让它接收时间。
from datetime import datetime, timezone
def build_row(data: dict, collected_at: datetime) -> dict:
return {
"request_id": data["request_id"],
"collected_at": collected_at.isoformat(),
"status": data.get("status"),
"organic": data.get("organic", []),
}
调用方决定时间,测试里传一个固定值:
def test_build_row_keeps_request_id():
row = build_row(
{"request_id": "r1", "status": 0, "organic": []},
collected_at=datetime(2026, 9, 21, 6, 0, tzinfo=timezone.utc),
)
assert row["request_id"] == "r1"
assert row["collected_at"].startswith("2026-09-21")
同样的输入永远得到同样的输出,测试不再受运行时刻影响。
睡眠和重试也要注入
重试逻辑里常见的 time.sleep(backoff) 会让测试变慢,而且无法断言「退避了多久」。
把 sleep 也变成可注入的:
def fetch_with_retry(client, q: str, sleep=time.sleep):
for attempt in range(3):
data = client.search(q)
if data.get("status") == 0:
return data
sleep(2 ** attempt)
raise RuntimeError("retries exhausted")
测试传一个记录调用的假 sleep:
def test_backoff_schedule():
calls = []
with pytest.raises(RuntimeError):
fetch_with_retry(FailingClient(), "x", sleep=calls.append)
assert calls == [1, 2, 4]
重试次数、退避节奏都能被断言,测试还是毫秒级。
uuid4() 也是隐藏依赖。测试里需要固定 request_id 时,不要断言具体值,而是断言「被透传了」;或者把生成器注入进来,测试时用递增序列。
原则一样:不确定的来源,要么断言它的形状,要么把它变成参数。
我做错的一版
最早用 freezegun 的 freeze_time 装饰器去「冻结时间」。测试跑通了,但代码里还是写着 datetime.now(),只是被全局替换了。
问题在于依赖没消失,只是被藏起来了。后来换环境、换测试框架,冻结失效,同一批测试又开始飘。
改成显式传时间之后,函数签名本身就说明了依赖,读代码的人一眼就知道「这里需要时钟」。
一个判断标准
写函数时问一句:同样的输入调用两次,结果一样吗? 如果不一样,找一下是谁在偷偷提供变化——时间、随机数、网络、全局状态。把它提到参数里,测试自然可复现。
一次网页搜索 1 credit,新账号 500 免费 credits,测试用替身不花额度,真实请求留给集成测试。
SerpBase 的字段结构见 SerpBase 文档。

浙公网安备 33010602011771号