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 文档。

posted @ 2026-11-03 11:30  蜘蛛人  阅读(6)  评论(0)    收藏  举报