RPA测试升级:pytest与xdoctest实现文档即测试

发布时间:2026/10/21 9:48:53
RPA测试升级:pytest与xdoctest实现文档即测试
开篇RPA 流程上线后测试居然是最薄弱的环节这两年 RPA 项目做得多了我发现一个很尴尬的事实流程开发顺手调试靠肉眼测试靠感觉。业务跑挂了就翻日志日志看不出来就开远程桌面看回放——这哪是测试这是考古。尤其流程一多、版本一迭代回归验证基本靠祈祷。后来我重新整理了工作流把 Python 生态里的 pytest 和 pytest-xdoctest 两个工具接进 RPA 项目的日常开发和验收环节用 7 步搭出一套“文档即测试、测试即文档”的自动化工作流。这条路径不挑平台不管你是用某个 RPA 厂商的编排工具还是纯 Python 自己搭流程底子都可以迁过去。这篇文章就把每一步的落地过程、参数设计、踩过的坑完整写出来供同样在 RPA 项目里被测试折磨的朋友直接照抄。文章适合三类人一类是 RPA 开发者想把流程验证从“手动点一遍”升级成“脚本自动跑一遍”一类是测试工程师想了解怎么把文档式测试引入 RPA 这种偏业务逻辑的环境还有一类是技术负责人需要一个低成本、高覆盖率的回归策略不想为了做测试再引入一堆新框架。七步流程走完你可以得到一套能自动发现测试用例、能直接从函数文档字符串提取案例、能生成覆盖率报告、能接入持续集成的完整工作流。这几个能力单拆开来都不稀奇但串在一起RPA 项目的测试体验会有质的提升。1. 为什么把 pytest 和 xdoctest 绑进 RPA 项目先想清楚在解决什么问题1.1 RPA 流程测试的现实困境RPA 流程和普通 Web 后端服务有个本质区别普通服务有明确输入输出接口测一遍基本能验证函数级正确性RPA 流程通常散布在十几个甚至几十个组件里每个组件可能只有一段 Python 脚本、一个选择器配置、几个异常分支。它本质上是“过程式”的状态靠全局变量传结果落在 Excel、数据库、邮件这些外部系统里。这种结构决定了传统单元测试框架落地别扭得 mock 掉一整套业务环境光做测试替身就要写几百行。我见过太多项目RPA 机器人跑得飞快但代码库里一个测试文件都没有。也不是不想写是真不知道怎么下手。业务流的每一步都强依赖外部系统测试环境只有一套还要给业务团队共用——这种情况下硬上 unittest 或者 pytest 的传统优先级方法维护成本直接爆炸。1.2 pytest-xdoctest 真正解决的问题是什么pytest-xdoctest 不是又一个测试框架它是给“已有代码文档中的示例代码”执行的测试运行器。它解决了 RPA 项目里最让人头疼的问题示例代码要么没人跑要么跑起来没处放。RPA 项目里每个流程组件往往是一个 Python 文件文件里不可避免要写注释、写说明、写示例。这些信息以前只有人看机器不认。pytest-xdoctest 做的就是把文档字符串docstring里的示例代码提取出来当成真实测试用例去执行。这意味着你在写注释的时候就等于在写测试注释和测试不会再分家。举个例子我有一个流程组件叫“入库单号标准化”它接收格式混乱的入库单号输出规范格式。以前写文档长这样def normalize_inbound_no(raw_no): 标准化入库单号 示例: normalize_inbound_no(RK20240815-001) RK20240815-001 引入 pytest-xdoctest 之后这段示例不用另建测试文件直接运行 pytest 就能被自动收集执行。写文档的人被迫保证示例可运行——跑挂了整个流程的测试就被标记为失败。文档和代码的一致性从“靠自觉”变成了“靠机制”。1.3 这套组合适合什么规模的项目我自己的体会是适合流程组件 10 个以上、且每个组件有独立输入输出边界、团队里至少有一到两个人熟悉 Python 的项目。如果只是三五个组件的小流程集成这套工具链有点杀鸡用牛刀。但如果是集团层面的 RPA 平台流程数量在几十上百每个流程拆成组件库沉淀复用这套工作流的收益会非常明显——组件的文档示例能自动回归每次重构时受影响组件直接显示出哪些示例挂了哪些行为被改变了效率比全部人工回归高一个量级。2. 环境准备版本选型与安装细节2.1 Python 版本与依赖清单我的实践环境是 Python 3.10pytest 7.4pytest-xdoctest 1.1RPA 流程通过 venv 管理依赖隔离得比较干净。下面是核心依赖推荐版本工具库推荐版本说明pytest7.4 及以上测试收集和执行的基底pytest-xdoctest1.1 及以上提供 xdoctest 风格的文档测试执行能力pytest-cov4.1 及以上覆盖率统计RPA 项目里能看到哪些组件没有示例覆盖python-dotenv1.0 及以上管理环境变量避免把数据库连接信息写进测试用例安装命令很简单pip install pytest pytest-xdoctest pytest-cov python-dotenv有人问为什么不用 Python 自带的 doctest 模块。对比下来pytest-xdoctest 的解析能力和错误诊断体验好不少它能识别更丰富的 Python 表达式结果列表、字典、集合都能直接展示支持随机化测试顺序和超时控制还有更好的 unicode 兼容性。RPA 流程里最常见的就是处理中文单据、清洗杂乱的文本数据docstring 里一不小心就出现中文标点、全角字符这些在 pytest-xdoctest 里不会变成诡异的行为差异。2.2 RPA 项目代码组织的基线要求pytest-xdoctest 是“跑文档里的代码”前提是文档就在代码里。RPA 项目里的组件至少有一个 .py 文件文件里至少有一个函数或类。如果流程组件是用可视化编排工具搭的、脚本只是嵌在一个文本框里那得先把组件脚本抽成独立模块文件的组织要转向模块化。这一步躲不过去——不拆模块后面所有测试机制都无处附着。另外环境的变量管理要提前做好。很多 RPA 流程测试需要连测试环境数据库或者调业务 API这些信息不应该散落在测试用例里。我常用的做法是在项目根目录维护 .env.test 文件pytest 的 conftest.py 里统一加载import os from dotenv import load_dotenv load_dotenv(.env.test)这样测试运行时环境变量是确定的团队成员执行结果一致不会出现“我本地跑过了你本地挂了”的情况。2.3 安装后的快速验证装完之后先跑一次空测试确认插件被 pytest 正确收集到了pytest --version输出里应该出现 pytest-xdoctest 的版本信息。如果没有检查项目根目录是否有 pytest.ini 或 pyproject.toml 的 pytest 配置段xdoctest 插件依赖 addopts 里的参数来激活这个配置放下一节详细说明。3. 7 步工作流总览与核心设计逻辑这 7 步不是随便排的每一步解决一个层次的问题。第一步解决“测试跑在哪里”第二步解决“测试从哪来”后面的步骤解决“怎么保证测试持续有效”。整个链条是一个从环境到用例再到集成的递进关系。3.1 整体流程图解用文字版流程表示我把完整流程拆成七个可独立执行但前后衔接的阶段建立 pytest 配置文件与激活 xdoctest 插件拆分 RPA 组件为可测试的 Python 模块在组件的 docstring 中编写示例化测试用例编写 conftest.py 夹具与外部资源管理执行测试并分析覆盖率缺口解决常见执行偏差与排查失败接入持续集成让回归自动化这七个阶段映射到日常开发里基本就是开工前配置一次、写代码时顺手写示例、合代码前跑一遍、上线前让 CI 再兜一遍。3.2 为什么把“配置”放在最前面而不是写用例时再配这是很多人容易忽略的顺序问题。如果你先把 docstring 里的示例写好了回过头再来配 pytest 的 addopts中间会有一段很尴尬的时间窗口写好的示例没有被任何机制执行大家会以为已经生效了实际上还在裸奔。先花两分钟把配置文件建好再动手写组件测试能力从一开始就是在线状态。3.3 每组件的测试粒度如何把握xdoctest 适合函数级、组件级的小粒度验证不适合流程级的端到端测试。我在项目里实践出来的分界标准是函数输入输出简单、有明确预期值的写文档示例涉及跨系统交互、需要好几个步骤联动验证的单独放到 tests/ 目录下用普通 pytest 测试函数来写。比如“读取 Excel 清单并筛选状态为待办的订单”这种组件docstring 示例要给一个固定的小 Excel 路径但项目环境里不一定有这个文件容易因为环境差异挂掉。这种就不强行写进 docstring而是在 tests/ 下写带 fixture 的用例临时造一个 Excel 再验证。两条线互补既保证了文档同步又不牺牲可测性。4. 第一步pytest 配置文件的写法与插件激活4.1 最小可用配置创建一个 pytest.ini 文件放在项目根目录[pytest] addopts -p pytest_xdoctest testpaths rpa_components tests这里-p pytest_xdoctest是在启动时强制加载插件。之所以用-p而不是依赖插件自动发现是因为某些环境下自动发现会被其他插件的加载顺序影响直接-p最稳。testpaths指定了两个目录rpa_components是业务组件目录pytest 会扫描其中所有模块的 docstring 并执行示例tests是传统的测试目录放那些不适合写在 docstring 里的流程级用例。4.2 xdoctest 风格的核心配置项另一个推荐开两个选项[pytest] addopts -p pytest_xdoctest xdoctest_verbose 2 xdoctest_executor pytest xdoctest_rand 0 xdoctest_nest_import 1xdoctest_verbose 2展示执行的具体输入和输出对比失败时定位更快。xdoctest_executor pytest用 pytest 的断言机制进行结果对比报错信息更统一包含 pytest 风格的上下文。xdoctest_rand 0保持默认顺序执行RPA 项目里测试通常是按数据处理步骤串联的随机顺序短期内不适合。xdoctest_nest_import 1允许 docstring 里的示例直接使用模块内部尚未导入的名称避免示例里还要先补一堆 import 语句。后一个配置藏着一个细节它决定了示例代码能不能直接访问本模块内的其他函数。如果某个组件的 docstring 想展示辅助函数_clean_text的用法没有nest_import示例就得写from 某模块 import _clean_text这种冗余代码既不简洁也容易错。4.3 认真定义 pytest 的 markerRPA 项目里经常有需要连数据库的用例和纯逻辑用例之分。我给它们分了两组 markermarkers db需要真实数据库连接的用例 smoke冒烟级别用例跑得快的才打这个标记比如pytest.mark.smoke def test_normalize_inbound_no(): assert normalize_inbound_no(RK20240815-001) RK20240815-001冒烟用例可以在开发阶段频繁跑完整用例留给 CI。这一步不算必须但流程多了以后每天跑全部用例的时间从几秒涨到几分钟分批跑的痛苦你们会懂。5. 第二步到第四步模块拆分、docstring 编写与夹具管理5.1 RPA 组件模块化的实操模板假设现有一个流程是“每天的销售订单导入”。流程组件里有几个动作读取本地目录新文件、解析 Excel 内容、清洗字段、写数据库。以前是一个大脚本从上到下执行现在按职责拆成组件模块rpa_components/ ├── file_watcher.py ├── excel_parser.py ├── field_cleaner.py └── db_writer.py每个文件一个核心函数。比如field_cleaner.pydef clean_phone(phone): 去除非数字字符中国大陆手机号转为11位标准格式 示例: clean_phone(138 1234 5678) 13812345678 clean_phone(86 138-1234-5678) 13812345678 参数: phone: 原始手机号字符串 返回: 标准化后的手机号字符串 return .join(filter(str.isdigit, phone))[-11:]注意这里用了两个示例块一个是常规空格样式一个是带国际区号和连字符的杂样。RPA 清洗类组件是最容易直接从真实业务数据里提取案例的把这个习惯养成之后docstring 就不再是摆设每条示例都是一条自动化回归记录。5.2 docstring 编写的一套自己的格式规范在团队里推行 xdoctest 之前我先定了一套 docstring 格式规范避免每个人都按自己的风格写每个公开函数必须有示例至少一个正常路径和一个异常路径示例代码必须能从模块上下文直接执行不依赖未定义的中间变量期望输出与实际执行输出严格一致包括引号、空格、标点涉及环境差异的输出如绝对路径、时间戳用省略号或者固定 mock 数据代替示例中不要包含网络请求、数据库写入等副作用操作第 4 条尤其关键。RPA 流程里经常有返回“当前时间”的数据派生列如果 docstring 里写了datetime.now().isoformat()的期望值那每次跑测试都在赌自己的执行时间和写入文档时差了多久纯属自己给自己埋雷。我一般会做一个固定的日期参数函数里不直接调 now()而是接收一个as_of参数默认再取当前时间。这样示例里传固定日期输出完全可预测。5.3 conftest.py 里的 RPA 常用夹具集合conftest.py 是所有 tests/ 目录共享夹具的地方。我在 RPA 项目里最常用的夹具是“临时 Excel 文件”。import pandas as pd import pytest from pathlib import Path pytest.fixture def temp_excel(tmp_path): 在临时目录创建一张可直接读取的 Excel 表 file_path tmp_path / demo.xlsx df pd.DataFrame({ order_no: [A001, A002], status: [待办, 已完成], amount: [100.5, 268.0], }) df.to_excel(file_path, indexFalse) return str(file_path)测试函数里可以直接拿temp_excel传入组件函数做验证def test_parse_excel_with_fixture(temp_excel): rows parse_excel(temp_excel) assert len(rows) 2 assert rows[0][order_no] A001有了 fixturedocstring 里不涉及临时文件的情况大量流程级用例就可以在 tests/ 目录下复用同一个临时数据源不需要每个测试自己造数据。5.4 外部资源的环境隔离策略前面提到 .env.test这里补充 conftest.py 里怎么确保用例执行时不会误连生产资源。一个简单粗暴但也有效的策略启动 pytest 时检查关键环境变量如果取值包含prod或production直接抛异常终止。import os def pytest_configure(config): env os.getenv(RPA_ENV, test) if prod in env.lower(): raise RuntimeError(禁止在 prod 环境执行测试用例)RPA 项目最容易出事的场景是同一个机器人脚本在测试环境跑得好好的某次上线前忘了切环境变量直接跑用例结果往生产库写了几条脏数据。有了这层硬保护至少这类事故的概率会降一个等级。6. 第五步到第七步执行、回溯与持续集成6.1 执行测试时我常用的命令组合写文档示例的阶段执行命令通常分三层第一层跑全部包括 docstring 示例和 tests/ 目录pytest第二层只跑当前正在改的模块pytest --xdoctest-only rpa_components/field_cleaner.py第三层带覆盖率统计pytest --covrpa_components --cov-reportterm-missing在开发的日常循环里第二层用得最多因为它一次只验证一个组件反馈速度快不用每次等全部组件跑完。等一个组件稳定了再整包跑一遍全量。--xdoctest-only是 pytest-xdoctest 插件提供的参数它告诉 pytest 只执行 docstring 示例跳过 tests/ 目录下的普通测试函数。这个参数在排查某个单独组件的文档示例是否有问题时极其好用——你可以快速定位到是 docstring 的问题还是业务函数逻辑的问题。6.2 覆盖率报告的解读方式pytest-cov 生成的报告是排查盲区的第一手资料。我第一次在 RPA 项目里跑覆盖率统计时看到的结果很不体面核心的“单据解析”组件覆盖率只有 40%一半以上的分支从未被执行过。这意味着很多异常分支比如日期格式不对、缺字段、数据为空可能在线上真实场景里已经出现过若干次了但测试从来没有任何感知。覆盖率报告里要重点关注三类缺口纯组件文件里部分函数完全没有示例覆盖说明文档没有同步更新except分支没有被触发说明异常路径没有建示例同一个函数两个示例都没覆盖到某个 if/else 分支说明测试用例的多样性不够补这些缺口不是一次性行为而是每次需求变更时顺手把新分支补进 docstring 示例里。这样覆盖率会随着时间逐步增长而不是靠某天突击写一批用例来充数。6.3 接入持续集成的做法参考我的工作流里pytest 这一步放在 RPA 流程代码合入主干之前执行。CI 脚本大致是python -m venv .venv . .venv/bin/activate pip install -r requirements.txt pytest --covrpa_components --cov-reportxml --cov-fail-under70--cov-fail-under70的含义是如果总体覆盖率低于 70%构建直接失败阻止合入。这个阈值一开始可以定低一点团队维护一段时间后再逐步提高。我见过把阈值直接定到 90% 然后团队开始伪造测试各种做法最终覆盖率上去了测试质量反而下来了就为了凑数字。先跑通再提阈值节奏要现实。CI 环境里的关键一步是安装依赖时要用国内网络可用的镜像源避免因为网络问题导致依赖装不上拉长整个构建时间。比如pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple实测下来这能让 CI 从纯海外源配置下的四五分钟缩短到一分钟以内。6.4 一个完整的 CI 工作示例我用 Gitee Go 做持续集成比较多工作流文件大概长这样name: RPA CI on: push: branches: - main pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: 设置 Python uses: actions/setup-pythonv5 with: python-version: 3.10 - name: 安装依赖 run: | pip install --upgrade pip pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple - name: 设置环境变量 run: | cp .env.test.example .env.test - name: 运行测试 run: | pytest --covrpa_components --cov-reportxml --cov-fail-under70核心就是四步拉代码、装依赖、准备环境文件、跑测试。这套流水线建好后每个 MR 提交时都会自动触发RPA 流程组件的回归测试从此不再依赖任何人记得“要跑一下”。7. 常见问题与排查技巧实录7.1 docstring 示例执行结果和预期不一致这是最常见的报错形式是“Expected ... Got ...”但很多人会忽略一个细节xdoctest 显示期望结果的顺序与 mind 中写的顺序未必一致如果示例里是列表或者字典顺序变化会影响结果。解决方式有两个一是把结果转换成排序后的列表再显示 sorted(get_orders(待办)) [A001, A002]二是直接返回集合 set(get_orders(待办)) {A001, A002}RPA 业务里经常要处理顺序不稳定的数据集这种小技巧能大幅减少日志排查时间千万别嫌麻烦。7.2 中文字符或全角标点引起的失败docstring 示例里的期望输出如果带有中文全角符号比如中文逗号、中文冒号、括号写的时候特别容易和半角混用。xdoctest 对字符差异是严格敏感的一个全角空格都会导致断言失败。我的习惯是所有 docstring 里的标点统一用半角除非期望输出本身就是中文原样内容。这样可以减少一半以上因为“到底几个空格”引发的时间浪费。另外pytest-xdoctest 对 unicode 的兼容性已经比原生 doctest 好很多但如果遇到 unicode 编码问题检查终端和 CI 环境的默认编码统一设置为 UTF-8export PYTHONUTF81在 CI 脚本里加上这一行中文相关示例基本不会再因为编码报错。7.3 运行 pytest 后 docstring 示例没有被执行这是一个高频问题docstring 写了但 pytest 跑完显示“no tests ran”或者只有 tests/ 目录的用例数。排查思路按顺序来确认pytest.ini中addopts -p pytest_xdoctest是否生效在命令行执行pytest --trace-config看输出是否加载了 pytest_xdoctest确认模块文件是否在testpaths指定的目录里确认 docstring 里示例的缩进是否统一xdoctest 要求示例代码块顶格写到 docstring 三引号内的最左边不能是四空格或者一个 tab 混合确认函数名是否以test_、check_之类的前缀开头并不是必要条件xdoctest 不需要以 test 开头它扫描所有 docstring7.4 失败堆栈指向并不直观xdoctest 的失败堆栈往往指向原始模块的 docstring 行号而不是一个独立的测试函数名。初用者容易懵不知道怎么定位。我的做法是在 docstring 的每个示例块里加一行注释说明这个示例的业务场景def generate_invoice_no(order_id): 生成发票号 示例: # 正常订单 generate_invoice_no(A001) FP-A001-2024 # 缺省订单号时返回兜底值 generate_invoice_no() FP-NONE-2024 这样失败时通过注释能快速知道是哪个业务分支出了问题不必再逐行读 docstring。团队规模一大这种注释带来的定位效率提升非常明显。7.5 测试执行顺序不稳定导致的连带失败docstring 示例有时会操作同一个外部文件或环境变量如果前一个示例修改了全局状态后一个示例再断言就会飘。建议写 docstring 示例时严格遵守“无副作用”原则只对内存数据做操作不写文件、不连数据库、不修改全局变量。确实需要持久化验证的场景放到 tests/ 目录下用 fixture 管理状态而不是塞进 docstring。规则虽然简单但严格执行下来的项目基本上没再出现过“单独跑能过、全量跑就挂”的诡异情况。7.6 依赖安装或版本冲突的常见处理RPA 项目常依赖各种办公自动化库比如操作 Excel、PDF 的库这些库和 pytest 系列有时会有间接依赖冲突。遇到诡异的安装失败时推荐试试通过 virtualenv 做隔离环境然后先装核心库再装测试库把顺序反过来往往能避开解析冲突。还有一个技巧是锁 pytest 的次要版本比如pytest7.4.*避免新版本插件不兼容时升级带来的连带问题。8. 实操心得这些坑我是真实踩过的整套工作流从搭建到现在稳定运行有几个体会不吐不快。第一个体会是把测试用例写进 docstring对写代码的人要求其实很高。它逼着你在写函数的同时就想清楚输入输出契约而不是等别人来猜你的函数意图。一个函数如果 docstring 示例写不出来往往意味着函数设计有问题——参数太多、职责太杂、边界太模糊。这个时候与其硬写示例不如重构函数本身。我遇到过好多次函数拆小之后示例自然就清晰了。这个“文档倒逼设计”的额外收益是当初集成 pytest-xdoctest 时没有预料到的。第二个体会是覆盖率阈值要分阶段提不要一步到高位。最开始把失败线定在 60% 也没关系关键是让全队养成“每次改代码顺手补示例”的习惯。两个月后你会惊喜地发现覆盖率不知不觉已经过了 80%——因为组件文档已经和实现同频了。相反一上来就要求 90%很容易逼着团队写出“为了覆盖而覆盖”的无效示例纯粹占用 CI 时间没有任何实际保护效果。第三个体会是不要把所有测试都塞进 docstring。RPA 项目的端到端场景、跨系统集成场景还是得乖乖写 tests/ 目录下的完整测试用例配合 fixture 和 mock否则 docstring 会膨胀到没法读。我的分界标准很粗暴一个示例超过 10 行或者需要三个以上前置变量就不要放 docstring直接进 tests/。这条线在团队里执行后docstring 始终保持精简tests/ 目录拥有更复杂的场景覆盖两者各司其职。目前这套工作流已经在我负责的多个 RPA 项目里跑了大半年最直接的收益是每次流程重构后回归验证从半天缩短到十几分钟。以前靠手工点验的场景现在用 pytest 一键全量跑完加上覆盖率报告哪些分支没覆盖一目了然。如果你手上的 RPA 项目也正处在“文档是文档、测试是测试两边谁也不管谁”的尴尬阶段可以试试按这 7 步把这套机制引进去。连接好 pytest、pytest-xdoctest 和你的项目工程剩下的事情就是让写代码的人为自己写的每段注释负责。