ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Crawlee Python 单元测试稳定性治理:pytest 标记与 CI 并行环境下的 Flaky 测试处理指南

Crawlee Python 单元测试稳定性治理:pytest 标记与 CI 并行环境下的 Flaky 测试处理指南 Crawlee Python 单元测试稳定性治理pytest 标记与 CI 并行环境下的 Flaky 测试处理指南【免费下载链接】crawlee-pythonCrawlee—A web scraping and browser automation library for Python to build reliable crawlers. Extract data for AI, LLMs, RAG, or GPTs. Download HTML, PDF, JPG, PNG, and other files from websites. Works with Parsel, BeautifulSoup, Playwright, and raw HTTP. Both headful and headless mode. With proxy rotation.项目地址: https://gitcode.com/GitHub_Trending/cr/crawlee-python本篇技术指南以 tests/unit/README.md 为核心骨架系统讲解 CrawleePython 版 Web 爬取与浏览器自动化库单元测试套件在 CI 并行环境下如何处理易失败flaky测试从“先查根因、再谈标记”的处理优先级到run_alone_on_mac、run_alone、pytest.mark.flaky、pytest.mark.skip四种 pytest 标记的适用场景与底层实现。读者将掌握一套可直接复用的测试稳定性治理方法论并理解 Crawlee 仓库中 xdist 并行调度、跨平台 CI 矩阵与测试隔离机制是如何协同工作的。一、背景为什么单元测试会“偶发失败”Crawlee 的单元测试套件规模庞大覆盖爬虫核心BasicCrawler、Playwright、BeautifulSoup、HTTP 客户端、自动扩缩容、会话管理、存储层等模块。这些测试在 CI 中运行时存在一个反复出现的现象某些测试在本地稳定通过却在 CI 上偶发失败。根据 tests/unit/README.md 的说明这种偶发失败flaky behavior本身就是一个值得重视的信号其原因通常可以分为两类代码缺陷或测试设计缺陷测试偶发失败可能暗示被测代码存在时序、竞态或资源管理 bug也可能是测试自身的断言方式、隔离性设计不合理测试执行环境的客观限制包括测试之间未能完全隔离共享了全局状态、端口、文件或浏览器进程、CI 执行器的资源约束内存、CPU 被并行 worker 挤占等。从仓库的 CI 配置可以直观看到这种并行压力来自何处。.github/workflows/_checks.yaml 中的unit_tests任务在Ubuntu、Windows、macOS 三个操作系统上、以Python 3.10 至 3.14 五个版本的矩阵运行测试并设置tests_concurrency: 8——这意味着同一时刻有大量测试 worker 并行执行任何资源敏感型测试真实浏览器启动、内存读数断言、端口绑定都可能互相干扰。二、核心原则先查根因后谈缓解Crawlee 团队在文档中给出的第一原则是面对 flaky 测试首要任务是理解其失败原因——因为这可能指向代码中的 bug或测试设计中的缺陷。因此处理 flaky 测试的建议手段按优先级严格排序调查根因并修复代码或测试本身最高优先级若无法立即修复根因应用 pytest 标记缓解偶发性最后兜底才是跳过测试。这一原则在仓库中有大量呼应。例如 tests/unit/conftest.py 通过autouseTrue的 fixture 在测试期间统一压制UserWarning主要针对 SqlStorageClient 的实验性状态警告并配合_isolate_test_environmentfixture 在每个测试前后重置全局状态重置 service locator、清空存储实例缓存、归零Statistics与BasicCrawler的类级计数器、将存储目录指向tmp_path从源头上减少因测试间共享全局状态而引发的 flaky——这正是“查根因、修设计”思想的制度化落地。三、四种 pytest 标记的适用场景与使用规则当根因调查无法立即完成、或资源约束无法根除时文档给出了四种按优先级排列的 pytest 标记方案3.1run_alone_on_mac仅在 macOS 上串行执行带此标记的测试在 CI 的 macOS 执行器上单独运行正常情况下多个测试并行执行可能导致资源敏感型测试失败适用于已知仅在 macOS 上偶发失败的资源敏感型测试。该标记的实际定义位于 tests/unit/utils.pyrun_alone_on_mac pytest.mark.run_alone if sys.platform darwin else lambda x: x从源码可以看到其精妙之处它在非 macOS 平台如 Linux、Windows上是一个恒等函数no-op不影响测试原本的并行调度只有当前平台是 macOSsys.platform darwin时才真正挂上pytest.mark.run_alone标记使测试在 macOS 执行器上退化为单独串行运行。这避免了让所有平台的测试都失去并行能力实现了“按需降级”。3.2run_alone在所有执行器上独立运行带此标记的测试在任何执行器上都会单独运行适用于已知在所有平台都易失败、或因测试设计原因无法与其他测试并行的资源敏感型测试文档强调这种情况“应极其罕见”。run_alone是仓库中唯一通过 pytest 配置显式注册的自定义标记见 pyproject.tomlmarkers [ run_alone: marks tests that must run in isolation, ]仓库中有大量真实用例。最典型的是 tests/unit/_utils/test_system.py 中的内存估算测试# The estimation is asserted on absolute memory readings, which hold only as long as nothing else on the machine makes # the kernel reclaim the pages allocated below. Running alongside the other test workers is enough to break that. pytest.mark.run_alone pytest.mark.skipif(sys.platform ! linux, reasonImproved estimation available only on Linux) def test_memory_estimation_does_not_overestimate_due_to_shared_memory() - None:注释非常直白该测试基于绝对内存读数做断言只要机器上其他测试 worker 导致内核回收了已分配的内存页断言就会失败——即“并行本身就会破坏测试前提”。这类测试天然无法并行必须打上run_alone。同类用例还包括 tests/unit/browsers/test_browser_pool.py 中启动真实 Firefox 浏览器的测试注释指出“启动真实浏览器资源开销大在 xdist 并行下会超时”、tests/unit/_autoscaling/test_autoscaled_pool.py 的并发运行测试、tests/unit/_autoscaling/test_snapshotter.py 的 CPU 采样测试以及 tests/unit/crawlers/_playwright/test_playwright_crawler.py 中 Firefox headless 请求头等测试。3.3pytest.mark.flaky失败后自动重试带此标记的测试在失败后会被重试若干次适用于已知偶发失败、但失败原因尚未查明或难以缓解的测试。该标记由 dev 依赖中的pytest-rerunfailures插件提供见 pyproject.toml 的pytest-rerunfailures17.0.0。一个教科书式的组合用法出现在 tests/unit/crawlers/_basic/test_basic_crawler.pypytest.mark.run_alone pytest.mark.flaky( reruns3, reasonTest is flaky on Windows and MacOS, see https://github.com/apify/crawlee-python/issues/1652. ) pytest.mark.skipif(sys.version_info[:3] (3, 11), reasonasyncio.timeout was introduced in Python 3.11.) pytest.mark.parametrize( sleep_type, [ pytest.param(async_sleep), pytest.param(sync_sleep, markspytest.mark.skip(reasonhttps://github.com/apify/crawlee-python/issues/908)), ], ) async def test_timeout_in_handler(sleep_type: str) - None:这段代码几乎是整套标记体系的“全家福”run_alone该测试涉及 handler 超时与重试语义对时序敏感先保证隔离pytest.mark.flaky(reruns3, ...)明确记录在 Windows 和 macOS 上存在已知偶发失败并附 issue 链接失败自动重试 3 次pytest.mark.skipifasyncio.timeout是 Python 3.11 才引入的 API因此在旧版本上直接跳过pytest.mark.skip通过pytest.param内嵌sync_sleep参数组合存在未解决的 issue暂时跳过该参数化分支。这种“多层标记叠加”的模式清晰展示了各标记的分工边界隔离run_alone解决环境干扰重试flaky兜底未查明原因的不稳定跳过skip处理版本兼容与已知未修复问题。3.4pytest.mark.skip最后的兜底手段带此标记的测试会被跳过。文档明确强调只有在以上所有手段都无法缓解时才应使用 skip因为跳过测试会隐藏潜在 bug 并制造虚假的安全感false sense of security。被跳过的测试必须在 GitHub issue 中登记追踪以便后续恢复。在 tests/unit/crawlers/_basic/test_basic_crawler.py 中可以看到跳过总是带着明确的reason如指向 issue 的链接这正是“被跳过的测试要可追踪”原则的体现——任何跳过都不是无理由的、临时的而是有据可查、可回溯、未来可恢复的。四、标记背后的调度机制串行前置 并行分流run_alone标记并非 pytest 的内置行为而是仓库在测试任务编排层赋予它的语义。查看 pyproject.toml 中的 Poe 任务定义即可看清uv run pytest \ -m run_alone \ tests/unit \ uv run pytest \ --numprocesses${TESTS_CONCURRENCY:-auto} \ -m not run_alone \ tests/unit执行unit-tests任务时测试被拆分为两个阶段第一阶段仅运行带run_alone标记的测试-m run_alone此时不指定--numprocesses即串行执行确保资源敏感型测试独占整个执行器第二阶段运行其余所有测试-m not run_alone并通过--numprocesses${TESTS_CONCURRENCY:-auto}以 xdist 并行调度本地默认自动探测 CPU 核数CI 中由tests_concurrency控制。同时pyproject.toml 中的 pytest 全局配置还包含addopts -r a --verbose --dist worksteal asyncio_default_fixture_loop_scope function asyncio_mode auto timeout 1800--dist worksteal使用 xdist 的 worksteal 调度策略动态均衡各 worker 的负载避免某些 worker 积压、另一些空转asyncio_mode auto与asyncio_default_fixture_loop_scope function配合 pytest-asyncio使异步测试与 fixture 的循环作用域得到统一管理timeout 1800配合 pytest-timeout 为单个测试设置半小时超时防止失控测试拖垮整个任务另有 filterwarnings 针对 Uvicorn 内部依赖的websockets弃用警告做定向忽略避免噪音干扰失败归因。这一“先串行跑隔离测试再并行跑其余测试”的两阶段编排是run_alone语义能够落地的关键基础设施也是本仓库治理 flaky 测试最具工程参考价值的部分。五、从根因层面降低 flaky 的配套实践除了标记体系Crawlee 仓库还在测试基建层面提供了多种“治本”手段与文档的优先级原则形成互补。5.1 全局状态自动隔离tests/unit/conftest.py 中的prepare_test_env与_isolate_test_environment均为autouseTrue保证每个测试都在干净环境中启动设置CRAWLEE_DISABLE_BROWSER_SANDBOXCI 环境无法使用浏览器沙箱、将CRAWLEE_STORAGE_DIR指向tmp_path、重置 service locator 的三个内部状态、清空存储实例缓存、归零类级计数器。这消除了测试间最普遍的一类 flaky 源头——全局状态污染。5.2 轮询等待替代固定 sleeptests/unit/utils.py 提供的poll_until_condition辅助函数明确建议“用条件轮询替代固定asyncio.sleep”来等待某个状态收敛例如自动扩缩容的并发度变化并支持backoff_factor指数退避async def poll_until_condition( fn: Callable[[], Awaitable[T] | T], condition: Callable[[T], bool] bool, *, timeout: float 5, poll_interval: float 0.05, backoff_factor: float 1, ) - T:固定 sleep 的时长是拍脑袋定的机器负载高时不够、负载低时浪费而基于真实条件的轮询在“状态未就绪”与“状态已就绪”之间自适应从根本上减少了时序类 flaky。5.3 测试内资源降级例如 tests/unit/conftest.py 启动本地代理Proxy时强制--num-workers 1 --num-acceptors 1注释明确解释默认情况下每个 CPU 核都会派生一个 acceptor 和 executor 进程这对单个测试用的代理是浪费且会在 CI 并行负载下压垮 xdist worker。这种“在测试内部主动收敛资源占用”的做法与文档所述“资源约束导致 flaky”的原因一一对应。六、小结与决策速查处理 Crawlee 仓库单元测试偶发失败时可按以下决策路径操作与 tests/unit/README.md 完全一致优先级手段适用场景仓库佐证1调查根因并修复一切 flaky 的第一选择全局状态隔离 fixture、轮询等待替代固定 sleep2run_alone_on_mac仅 macOS 上资源敏感的测试定义于 tests/unit/utils.py非 darwin 平台为 no-op2run_alone所有平台资源敏感、无法并行的测试内存估算、Firefox 启动等测试Poe 任务串行前置执行3pytest.mark.flaky已知失败但原因未明pytest-rerunfailures提供如reruns3的 handler 超时测试4pytest.mark.skip最后手段必须带 reason 并登记 issue参数化分支内嵌 skip如sync_sleep分支最后再次强调文档的核心结论skip 永远不是免费的午餐——它只是把问题从测试失败列表转移到了 GitHub issue 列表掩盖了潜在的代码 bug只有“理解失败原因”才能真正带来测试套件的长期健康。本仓库将上述标记、两阶段测试编排见 pyproject.toml 的unit-tests与unit-tests-coverage任务与隔离基建组合使用为构建大规模、跨平台、可并行的爬虫库测试套件提供了一个值得借鉴的完整范例。【免费下载链接】crawlee-pythonCrawlee—A web scraping and browser automation library for Python to build reliable crawlers. Extract data for AI, LLMs, RAG, or GPTs. Download HTML, PDF, JPG, PNG, and other files from websites. Works with Parsel, BeautifulSoup, Playwright, and raw HTTP. Both headful and headless mode. With proxy rotation.项目地址: https://gitcode.com/GitHub_Trending/cr/crawlee-python创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表