测试Pipeline说明.md
6.44 KB
搜索引擎测试流水线指南
本文档是测试套件的权威入口,涵盖目录约定、运行方式、回归锚点矩阵、以及手动
联调脚本的分工。任何与这里不一致的历史文档(例如提到 tests/unit/ 或
scripts/start_test_environment.sh)都是过期信息,以本文为准。
1. 测试目录与分层
tests/
├── conftest.py # 只做 sys.path 注入;不再维护全局 fixture
├── ci/ # API/服务契约(FastAPI TestClient + 全 fake 依赖)
│ └── test_service_api_contracts.py
├── manual/ # 需真实服务才能跑的联调脚本,pytest 默认不 collect
│ ├── test_build_docs_api.py
│ ├── test_cnclip_service.py
│ └── test_facet_api.py
└── test_*.py # 子系统单测(全部自带 fake,无外部依赖)
关键约束(写在 pytest.ini 里,不要另起分支):
testpaths = tests,norecursedirs = tests/manual;--strict-markers:所有 marker 必须先在pytest.ini::markers登记;- 测试不得依赖真实 ES / DeepL / LLM 服务。需要外部依赖的脚本请放
tests/manual/。
2. 运行方式
| 场景 | 命令 | 覆盖范围 |
|---|---|---|
| CI 门禁(每次提交) | ./scripts/run_ci_tests.sh |
tests/ci + contract marker + search ∧ regression |
| 发版 / 大合并前 | ./scripts/run_regression_tests.sh |
所有 @pytest.mark.regression |
| 子系统子集 | SUBSYSTEM=search ./scripts/run_regression_tests.sh |
指定子系统的 regression 锚点 |
| 全量(含非回归) | python -m pytest tests/ -q |
全部自动化用例 |
| 手动联调 | python tests/manual/<script>.py |
需提前起对应服务 |
3. Marker 体系与回归锚点矩阵
marker 定义见 pytest.ini。每个测试文件通过模块级 pytestmark 贴标,同时
属于 regression 的用例构成“回归锚点集合”。
| 子系统 marker | 关键文件(锚点) | 保护的行为 |
|---|---|---|
contract |
tests/ci/test_service_api_contracts.py |
Search / Indexer / Embedding / Reranker / Translation 的 HTTP 契约 |
search |
test_search_rerank_window.py, test_es_query_builder.py, test_es_query_builder_text_recall_languages.py |
Searcher 主路径、排序 / 召回、keywords 副 combined_fields、多语种 |
query |
test_query_parser_mixed_language.py, test_tokenization.py |
中英混合解析、HanLP 分词、language detect |
intent |
test_style_intent.py, test_product_title_exclusion.py, test_sku_intent_selector.py |
风格意图、商品标题排除、SKU 选型 |
rerank |
test_rerank_client.py, test_rerank_query_text.py, test_rerank_provider_topn.py, test_reranker_server_topn.py, test_reranker_dashscope_backend.py, test_reranker_qwen3_gguf_backend.py |
粗排 / 精排 / topN / 后端切换 |
embedding |
test_embedding_pipeline.py, test_embedding_service_limits.py, test_embedding_service_priority.py, test_cache_keys.py |
文本/图像向量客户端、inflight limiter、优先级队列、缓存 key |
translation |
test_translation_deepl_backend.py, test_translation_llm_backend.py, test_translation_local_backends.py, test_translator_failure_semantics.py |
DeepL / LLM / 本地回退、失败语义 |
indexer |
test_product_enrich_partial_mode.py, test_process_products_batching.py, test_llm_enrichment_batch_fill.py |
LLM Partial Mode、batch 拆分、空结果补位 |
suggestion |
test_suggestions.py |
建议索引构建 |
eval |
test_eval_metrics.py(regression) + test_search_evaluation_datasets.py / test_eval_framework_clients.py(非 regression) |
NDCG / ERR 指标、数据集加载、评估客户端 |
任何新写的子系统单测,都应该在顶部加
pytestmark = [pytest.mark.<子系统>, pytest.mark.regression]。 不贴regression的测试默认不会被run_regression_tests.sh选中,请谨慎决定。
4. 当前覆盖缺口(跟踪中)
以下场景目前没有被 regression 锚点覆盖,优先级从高到低:
api/routes/search.py的请求参数映射:QueryParser.parse(...)透传是否完整(目前只有tests/ci间接覆盖)。indexer/document_transformer.py的端到端转换:从 MySQL 行到 ES doc 的 snapshot 对比。config/loader.py加载多租户配置:含继承 / override 的合并规则。search/searcher.py::_build_function_score:function_score 装配。- Facet 聚合 / disjunctive 过滤。
- 图像搜索主路径(
search/image_searcher.py)。
补齐时记得同步贴 regression + 对应子系统 marker,并在本表删除条目。
5. 手动联调:索引文档构建流水线
除自动化测试外,联调/问题排查时建议走一遍“MySQL → ES doc”链路,确保字段与 mapping 与查询逻辑对齐。
cd /home/tw/saas-search
./scripts/stop.sh # 停掉已有进程(可选)
./scripts/start_indexer.sh # 启动 indexer 服务,默认端口 6004
curl -X POST "http://127.0.0.1:6004/indexer/build-docs-from-db" \
-H "Content-Type: application/json" \
-d '{ "tenant_id": "170", "spu_ids": ["223167"] }'
返回中 docs[0] 即当前代码构造的 ES doc(与 mappings/search_products.json 对齐)。
与真实 ES 数据对比的查询参考 docs/常用查询 - ES.md;若字段不一致,按以下路径定位:
indexer/document_transformer.py— 文档构造逻辑indexer/incremental_service.py— 增量查库逻辑logs/indexer.log— 索引日志
6. 编写测试的约束(与 开发原则 对齐)
- fail fast:测试输入不合法时应直接抛错,不用
if ... return;不要用try/except吃掉异常再assert not exception。 - 不做兼容双轨:用例对准当前实现,不为历史行为保留“旧 assert”。若确有外部兼容性(例如 API 上标注 Deprecated 的字段),在
tests/ci里单独写契约用例并注明 Deprecated。 - 外部依赖全 fake:凡是依赖 HTTP / Redis / ES / LLM 的测试必须注入 fake stub,否则归入
tests/manual/。 - 一处真相:共享 fixture 如果超过 2 个文件使用,放
tests/conftest.py;只给 1 个文件用就放在该文件内。避免再次出现全库无人引用的 dead fixture。