采集代码要不要测试?给搜索数据采集写测试的实践
很多人觉得采集代码「调接口 + 解析 JSON,跑起来就行,不用测试」。直到有一天字段解析挂掉、重试逻辑把请求打爆、或供应商改了返回结构——你才发现这些代码是真·生产代码,值得认真测。这篇记录我给搜索数据采集写测试的实践:mock 外部 API、管理 fixtures、测重试逻辑、应对字段变更。
为什么采集代码要测试
三个理由:
- 解析逻辑容易脆:字段可选、结构变化,一个
.get()用错就崩 - 重试/并发逻辑有边界情况:退避时间、重试次数、限流处理,写错会烧钱或打爆 QPS
- 改起来要敢改:没有测试,改一处解析怕挂三处
我用的数据源是 SerpBase(serpbase.dev),返回结构里有大量「可选字段」(snippet、display_url、position 等),正好是测试要覆盖的高危区。
第一步:mock 外部 API
测试采集代码,核心是别真的发请求——用 mock 返回固定的响应。Python 用 responses 库或 unittest.mock:
import requests
from unittest.mock import patch
# 被测试的采集函数
def fetch_organic(keyword):
resp = requests.post(
"https://api.serpbase.dev/google/search",
headers={"Content-Type": "application/json", "X-API-Key": "test-key"},
json={"q": keyword},
timeout=30,
)
data = resp.json()
return data.get("organic", [])
# 测试:mock 返回固定 JSON
@patch("requests.post")
def test_fetch_organic(mock_post):
mock_post.return_value.json.return_value = {
"status": 0,
"organic": [{"rank": 1, "title": "T", "link": "https://example.com"}],
}
result = fetch_organic("keyword")
assert result[0]["rank"] == 1
assert mock_post.called
mock 之后,测试跑得快、稳定、不消耗真实 credits。
第二步:管理 fixtures(真实响应样本)
mock 的响应要「像真的」。最好的做法是把真实响应存成 fixture 文件,测试直接读:
# tests/fixtures/search_full.json —— 从真实响应里保存一份
# {
# "status": 0,
# "organic": [
# {"rank": 1, "title": "...", "link": "...", "snippet": "..."}
# ],
# "related_searches": ["..."],
# "people_also_ask": [{"question": "..."}]
# }
import json, pathlib
FIXTURES = pathlib.Path(__file__).parent / "fixtures"
def load_fixture(name):
return json.loads((FIXTURES / name).read_text())
def test_parse_full_response():
data = load_fixture("search_full.json")
organic = data["organic"]
assert organic[0]["link"].startswith("https://")
fixture 的价值:它代表「当时真实的返回结构」。供应商改结构后,旧的 fixture 跑不过,你就知道「返回结构变了,需要更新解析」——这就是测试在帮你看住供应商变更。
第三步:重点测「可选字段」
SerpBase 文档明确标注很多字段是 optional(snippet、position、display_url、thumbnail 等)。解析代码必须用 .get() 兜底,测试就要覆盖「字段缺失」的场景:
def test_missing_optional_fields():
# 一个只有 rank 和 title、其他都缺的响应
data = {
"status": 0,
"organic": [{"rank": 1, "title": "Only essentials"}],
}
items = parse_organic(data)
assert items[0]["snippet"] == "" # 缺字段要兜底成空串
assert items[0]["position"] is None # 别 KeyError
这种「字段缺失」测试,就是防线上 KeyError 的第一道防线。
第四步:测重试逻辑
重试是采集代码最容易写错、也最怕写错的部分(写错要么烧 credits、要么把限流打爆)。测试要覆盖:
from unittest.mock import patch
import time
def test_retry_on_failure():
responses = [
{"status": 0, "organic": []}, # 第一次成功
]
# 用一个会先失败后成功的 mock
mock = MagicMock()
mock.json.side_effect = [
{"status": 500, "error": "upstream"},
{"status": 0, "organic": [{"rank": 1}]},
]
with patch("requests.post", return_value=mock):
result = call_with_retry("kw")
assert result["status"] == 0
assert mock.call_count == 2 # 失败后重试了一次
再测「重试次数上限」:连续失败 N 次后抛异常,而不是无限重试。
第五步:不测什么(同样重要)
- 不测供应商的行为:不测 SerpBase 是不是真的返回那些字段——那是它的责任,你的测试只管「拿到数据后怎么处理」
- 不测网络层:不测真实 HTTP,全部 mock
- 测试要快:mock 之后全测试应在秒级跑完,慢了没人跑
踩坑记录
坑 1:fixture 用的是「理想响应」,不是真实响应。 一开始手写 fixture,字段齐全得像教程,结果解析代码在真实数据上崩。后来改成「从真实响应复制」fixture,才暴露了可选字段问题。
坑 2:重试测试没 mock 成功路径。 只测了「全失败」,没测「失败后成功」,结果退避逻辑里有个 bug 直到线上才暴露。
坑 3:断言写得太松。 assert "link" in result 通过,但 link 是 None 也通过。断言要写到「值的类型和关键字段」,别只查「键存在」。
测试清单沉淀
- mock 外部 API,测试不依赖真实请求
- fixture 用真实响应样本,盯住供应商结构变更
- 全覆盖可选字段缺失场景,用
.get()兜底 - 测重试:失败重试、上限终止、成功路径
- 断言要严格到「值」,不要只查「键」
采集代码值得测试,因为它是「脆解析 + 外部依赖 + 重试并发」的组合——三类高危的叠加。花半天把测试补上,后续改字段解析、调重试策略都敢动手了。
完整响应字段(哪些可选、哪些必选)在 SerpBase 官方文档,写断言时先对照它。你的采集代码有测试吗?没有的话,从「mock + fixture」这两个最基础的开始。

浙公网安备 33010602011771号