采集代码要不要测试?给搜索数据采集写测试的实践

很多人觉得采集代码「调接口 + 解析 JSON,跑起来就行,不用测试」。直到有一天字段解析挂掉、重试逻辑把请求打爆、或供应商改了返回结构——你才发现这些代码是真·生产代码,值得认真测。这篇记录我给搜索数据采集写测试的实践:mock 外部 API、管理 fixtures、测重试逻辑、应对字段变更。

为什么采集代码要测试

三个理由:

  1. 解析逻辑容易脆:字段可选、结构变化,一个 .get() 用错就崩
  2. 重试/并发逻辑有边界情况:退避时间、重试次数、限流处理,写错会烧钱或打爆 QPS
  3. 改起来要敢改:没有测试,改一处解析怕挂三处

我用的数据源是 SerpBase(serpbase.dev),返回结构里有大量「可选字段」(snippetdisplay_urlposition 等),正好是测试要覆盖的高危区。

第一步: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(snippetpositiondisplay_urlthumbnail 等)。解析代码必须用 .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 也通过。断言要写到「值的类型和关键字段」,别只查「键存在」。

测试清单沉淀

  1. mock 外部 API,测试不依赖真实请求
  2. fixture 用真实响应样本,盯住供应商结构变更
  3. 全覆盖可选字段缺失场景,用 .get() 兜底
  4. 测重试:失败重试、上限终止、成功路径
  5. 断言要严格到「值」,不要只查「键」

采集代码值得测试,因为它是「脆解析 + 外部依赖 + 重试并发」的组合——三类高危的叠加。花半天把测试补上,后续改字段解析、调重试策略都敢动手了。

完整响应字段(哪些可选、哪些必选)在 SerpBase 官方文档,写断言时先对照它。你的采集代码有测试吗?没有的话,从「mock + fixture」这两个最基础的开始。

posted @ 2026-08-25 06:40  蜘蛛人  阅读(4)  评论(0)    收藏  举报