在测试这个行当里待得久了,你会发现一个规律:刚上手时大家比拼的是用例设计能力,写得出覆盖全面、逻辑严谨的用例就是高手;但做到后期,真正的分水岭往往是工程化能力——能不能把零散的脚本变成一套可持续运转的系统,能不能让团队里每个人都能低成本地创建、执行、跟踪自动化测试,而不是每次都要翻某位同事的电脑才能跑起来。
这也是我决定从零搭建一套自动化测试平台的原因。与其在多个工具之间来回切换、靠人工收集报告,不如把用例管理、任务调度、执行机调度、报告展示、质量数据统计这些能力收拢到一个统一入口里。它解决的核心问题有三个:让测试执行不需要依赖个人环境,让测试结果沉淀成可视化数据,让回归测试可以按计划自动触发。这篇文章我会把这个平台的完整搭建过程、技术选型逻辑、核心模块的设计思路,以及我在实际落地过程中踩过的坑,一次性讲清楚。适合正在做自动化测试建设、或者打算自己动手搭一套内部测试平台的团队参考。
1. 整体设计思路与方案选型
1.1 先想清楚平台要解决什么问题
很多团队在建设自动化测试平台时容易犯一个毛病:一上来就追求大而全,要在线编写脚本、要实时日志流、要做各种炫酷的仪表盘,结果项目拖了几个月还停留在 Demo 阶段。我个人的建议是,先界定 MVP(最小可行产品)边界,把“必须线上化”的能力先做掉,其他能力逐步迭代。
测试平台最核心的闭环其实只有四件事:用例怎么存、任务怎么跑、结果怎么收、报告怎么看。围绕这四条主线去拆解:
- 用例怎么存:需要一个结构化的用例仓库,支持按项目、模块、接口或页面维度组织。
- 任务怎么跑:用户发起一次测试执行,平台能把用例分发到可用的执行机上跑起来,并收集过程数据。
- 结果怎么收:执行过程中产生的日志、截图、断言结果、耗时等数据需要统一采集。
- 报告怎么看:执行完成后自动生成聚合报告,支持失败定位和历史对比。
我当时的设计原则是“平台只做调度和展示,执行交给独立脚本”。这样平台的代码逻辑相对简单,不需要在平台进程里跑测试逻辑,一旦某个用例执行器挂了,也不至于拖垮整个平台。
1.2 技术选型要考虑团队维护成本
选型这事没有标准答案,但有两条原则很关键:用团队最熟悉的技术栈,优先选择生态成熟的组件。
我最终定的方案是:
- 后端使用 Python 的 FastAPI 框架,提供 REST API。
- 前端使用 Vue 3 + Element Plus,搭建管理界面。
- 数据库使用 MySQL,用于存储用例、任务、执行记录等结构化数据。
- 缓存使用 Redis,用于存放待执行任务队列和执行机心跳信息。
- 调度器使用 APScheduler,处理定时任务触发。
- 执行机 Agent 使用 Python 编写,通过长连接或轮询方式向服务端领取任务。
这里有个值得展开的点:为什么调度器不直接用现成的 Jenkins?其实我也考虑过基于 Jenkins 去做二次开发,但后来发现 Jenkins 在“用例级调度”这个粒度上并不友好——Jenkins 的 Job 概念更偏向于一条流水线,而不是一组可灵活筛选的用例集合。我们的场景是用户勾选若干个用例,动态组成一个任务去跑,用自研调度器反而更灵活。当然,如果团队对 Jenkins 非常熟,用它做外围的定时触发、再回调平台 API 也是一种可行方案,这个没有必须二选一,只要接口分开就行。
前后端分离后,执行机 Agent 独立部署,通过注册机制接入平台。这样整个系统在物理上分成了三层:平台服务端、数据库/缓存等基础设施、执行机集群。我在实际部署中用一台 8C16G 的服务器承载平台服务端和 MySQL/Redis,执行机则按需在测试环境机器上部署 Agent。
2. 核心模块解析与交互流程设计
2.1 用例管理模块
用例管理是一个测试平台的“地基”。我在设计时没有把用例的数据结构搞得太复杂,考虑到自动化测试用例通常归属于某个项目、某个模块,并且自身有名称、优先级、类型、脚本路径等信息,我把用例表设计成了这样:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| project_id | bigint | 所属项目 ID |
| module_id | bigint | 所属模块 ID |
| name | varchar | 用例名称 |
| priority | tinyint | 优先级(P0/P1/P2) |
| test_type | tinyint | 用例类型(接口/UI/性能) |
| script_path | varchar | 脚本相对路径 |
| creator | varchar | 创建人 |
| status | tinyint | 启用/停用 |
脚本与用例分离是这里的关键决策。平台只记录脚本路径,具体的测试逻辑由仓库中的代码来维护。这样有两个好处:一是调试用例时不需要经过平台,本地直接跑代码就可以;二是执行机拉取最新代码后,用例脚本天然同步,不用在平台里维护一份代码副本。
对于测试脚本的编写,我推荐采用Page Object 模式来组织 UI 自动化,接口自动化则基于关键字驱动封装一层公共方法。比如接口测试会把“发送请求”“断言响应字段”“从响应中提取变量”封装成关键字,用例脚本里只需要描述业务场景,不需要重复处理请求细节。这个设计对后续让业务同事参与编写用例也有帮助。
2.2 任务调度模块
任务调度这一块是整个平台里最需要细心设计的。表面上看起来只是“触发执行”,实际上需要考虑:并发控制、超时处理、失败重试、执行机负载均衡。
我将调度逻辑分成了两部分:
定时触发:使用 APScheduler 创建定时任务,到时间点后调用任务创建接口,生成一条执行计划。
手动触发:用户在界面上勾选用例,点击“执行”,平台创建一条任务并放到 Redis 的等待队列里。
执行机 Agent 启动后会定期向服务端注册心跳,上报自己的存活状态、当前是否空闲、执行容量等信息。调度器在分发任务时,会从“在线且空闲”的执行机列表里选择合适的机器,把任务数据发过去。
这里要特别说明一下任务的执行状态流转。我把任务状态设计为:等待中 → 执行中 → 成功/失败/超时。其中“超时”这个状态非常有用,它由服务端侧的任务超时监测机制触发——每个任务在创建时会设定一个最大执行时长,超过这个时长还没收到完成回执,服务端直接判为超时,并允许重试或标记失败。这样即使执行机 Agent 崩溃,任务也不会一直卡死占用队列资源。
2.3 执行机 Agent 的设计
Agent 是执行任务的“手”,我把它设计成尽量轻量。它只做四件事:
- 启动时向服务端注册(上报执行机 ID、IP、支持的测试类型、当前状态)。
- 周期性发送心跳(同时作为获取新任务的一种机制)。
- 从服务端拉取分配给自己的任务,调用本地的执行命令来跑测试。
- 执行过程中上报日志片段,执行结束后回传结果文件路径和汇总数据。
Agent 与平台通信采用 HTTP API,没有用 WebSocket。原因很简单:HTTP 轮询的实现成本最低,出问题也最容易排查。虽然实时性差一些,但对于测试任务这种“分钟级”粒度的场景完全够用。
实际执行时,Agent 会在本地创建一个工作目录,拉取用例仓库指定分支的代码,然后通过命令行执行测试框架(接口测试我用 pytest,UI 测试我用 pytest + Selenium),并把 pytest 的 JUnit XML 报告和日志文件上传到平台的文件存储中。执行命令的具体构造我放在了服务端返回的任务数据里,这样调整执行参数时不需要升级 Agent。
2.4 报告与数据统计
执行完成后,平台需要把结构化结果展示出来。我的做法是:Agent 回传一份 JSON 格式的结果摘要(总用例数、通过数、失败数、耗时),平台把它写入数据库;同时原始日志与截图上传到文件目录,供用户进一步下载查看。
报告页面上我会展示五块信息:
- 本任务的执行概况卡片:通过率、总耗时、执行机、执行分支。
- 用例明细列表:每条用例的状态、耗时、失败原因、日志链接。
- 失败趋势折线图:最近若干次任务的失败率走势。
- 耗时分布:各用例耗时排序,找出拖慢回归的“钉子户”。
- 错误类型聚合:把常见异常信息聚类,快速定位是环境问题还是代码问题。
这块我可以推荐两个组件:图表直接用 ECharts,表格用前端组件自带的分页排序即可。数据查询接口按任务 ID 维度和时间维度各写一个,前端按需调用。
3. 实操过程与关键环节实现
3.1 搭建项目骨架
我建议不要一上来就写业务代码,先把项目工程建好,再逐层填充模块。整个服务端代码目录我大致是这样组织的:
test-platform/ ├── api/ # 路由层 │ ├── projects.py │ ├── cases.py │ ├── tasks.py │ ├── agents.py │ └── reports.py ├── core/ # 核心逻辑 │ ├── scheduler.py # 定时调度 │ ├── dispatcher.py # 任务分发 │ └── executor.py # 任务执行状态机 ├── models/ # ORM 模型 ├── schemas/ # 请求/响应模型 ├── services/ # 业务逻辑 ├── storage/ # 文件存储 └── main.py # 应用入口数据库表之间的核心关系可以这样概括:项目 → 模块 → 用例,任务 → 任务用例关联表 → 执行记录。我建了大概 12 张表,这里不全部贴 DDL,只把最关键的任务表结构展示出来,因为它承载了整个调度的主流程:
CREATE TABLE `test_task` ( `id` bigint NOT NULL AUTO_INCREMENT, `task_no` varchar(32) NOT NULL COMMENT '任务编号', `name` varchar(128) NOT NULL COMMENT '任务名称', `type` tinyint NOT NULL COMMENT '任务类型:1-手动 2-定时', `status` tinyint NOT NULL COMMENT '状态:0等待 1执行中 2成功 3失败 4超时', `trigger_type` varchar(16) DEFAULT NULL COMMENT '触发方式:manual/cron', `total_cases` int DEFAULT NULL COMMENT '用例总数', `pass_cases` int DEFAULT NULL COMMENT '通过数', `fail_cases` int DEFAULT NULL COMMENT '失败数', `executor_ip` varchar(64) DEFAULT NULL COMMENT '执行机IP', `start_time` datetime DEFAULT NULL, `end_time` datetime DEFAULT NULL, `create_by` varchar(64) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_task_no` (`task_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有个容易踩的细节:任务编号我用了日期+随机数的格式,比如T20250518153001A8K3。不要直接用自增 ID 当任务编号暴露给用户,不然很容易被猜到总量,也容易被外部批量调用。任务编号最好由服务端统一生成,并且加上一天内的唯一性校验。
3.2 任务分发链路实现
任务分发是整个系统最核心的执行链路。我在 dispatcher.py 里实现了一个轮询调度器,简单说就是:从 Redis 队列取任务,按顺序分配给当前在线的执行机。
核心逻辑伪代码如下:
def dispatch_task(task_data): online_agents = get_online_agents() available = [a for a in online_agents if a.is_idle] if not available: mark_task_waiting(task_data.task_no) return agent = available[0] # 简单轮训,也可以用权重 send_task_to_agent(agent, task_data) update_task_executor(task_data.task_no, agent.ip) mark_task_running(task_data.task_no)我在实际实现中并没有使用什么复杂的负载均衡算法,最初版本就是简单的列表轮询。因为执行机的任务执行时间通常在几分钟到几十分钟不等,相比高并发场景根本不是一个量级,简单的轮询已经能保证均衡。
Agent 端拉取任务的代码,我用 Flask 写了一个轻量接口,长这样:
@app.route('/api/v1/agent/task/poll', methods=['POST']) def poll_task(): agent_id = request.json.get('agent_id') agent_ip = request.json.get('agent_ip') task = get_task_for_agent(agent_id) if not task: return jsonify({'code': 0, 'data': None}) return jsonify({'code': 0, 'data': task})Agent 轮询间隔我设置为 5 秒。这个间隔经过了实际测试:再短意义不大,徒增服务端压力;再长会让任务分发有延迟感,用户在页面上点完执行要等 10 多秒才有反应。5 秒既能保证感知上的即时性,也给服务端留了余量。
3.3 执行机 Agent 的实现细节
Agent 收到任务后的执行流程,我整理成了一个顺序表:
- 创建本次任务的本地工作目录,命名格式为
{task_no}_{timestamp}。 - 拉取用例仓库指定分支的最新代码(如果仓库是 Git 管理的,先执行
git fetch和git checkout)。 - 根据任务类型选择执行方式:接口用例执行
pytest -m api,UI 用例执行pytest -m ui,带 pytest 的--junit-xml参数输出结构化报告。 - 执行过程中,将日志实时写入本地文件。
- 执行结束后解析 JUnit XML,统计出通过、失败、跳过、耗时等数据。
- 将日志、截图等产物打包上传到平台存储。
- 调用服务端回执接口,上报结果摘要,任务状态流转完成。
执行机环境是接触自动化测试平台的人最容易忽略的一环。我在搭建平台之前,先花了一周时间把执行机的统一环境搞好:Python 版本统一为 3.9+,浏览器统一装 Chrome 并锁定版本(用 Selenium 时必须保持 ChromeDriver 与浏览器版本匹配),依赖用 requirements.txt 锁定版本,并通过虚拟环境隔离项目依赖。执行机不要装一堆无关软件,也不要让多个项目共享同一个环境目录,干净的执行机是自动化稳定的前提。
3.4 定时任务与自动化触发
定时任务是自动化测试平台的重要价值所在,特别是对于需要每天凌晨跑回归的团队。
我在 API 层暴露了定时任务的 CRUD 接口,前端页面上用户可以创建一个“定时任务”,指定 cron 表达式和执行范围。例如每晚凌晨两点执行一次冒烟测试:
0 2 * * *创建时,平台不会直接依赖系统 crontab,而是把这条规则写入数据库的 cron 表,然后由调度器进程扫描数据库中的 cron 配置,在内存中注册到 APScheduler。
这里有个坑我需要提醒一下:APScheduler 的时区一定显式设置为本地时区,不要依赖系统默认。有次我在服务器上部署后,定时任务总是在比预期晚 8 小时触发,排查半天发现是调度器默认读取 UTC 时间,而业务上期望的是北京时间。这个属于“看似不可能但确实会发生”的问题。
此外,定时任务触发失败后要有告警。我在设计里把告警分成两层:任务触发失败告警和任务执行失败告警。前者是调度器调接口异常,后者是执行结果里失败率超过阈值。告警渠道选择了企业微信机器人 Webhook,在通知里带上任务编号、失败率、日志链接,团队查问题不用再登录平台搜索。
4. 常见问题与排查技巧实录
4.1 Agent 状态显示“离线”但机器明明是活的
这是平台搭建初期最频繁遇到的问题。现象是:页面上的执行机列表显示离线,但 SSH 到执行机查看,Agent 进程明明在跑,日志也没有异常。
排查思路分三步:
- 先看 Agent 到服务端的网络连通性:在 Agent 机器上用 curl 请求服务端心跳接口,确认不是防火墙拦截。
- 打开服务端日志,查看最近一次心跳处理记录。如果服务端根本没收到心跳请求,问题大概率出在 Agent 侧的网络或代码;如果收到了但心跳时间戳比较旧,说明 Agent 处理心跳的循环卡住了。
- 检查数据库中心跳时间字段是否有索引。如果没有索引,执行机数量多了以后时间查询会变慢,导致服务端判断心跳超时。
最终我们发现的原因非常“经典”:Agent 的日志里抛出了一个数据库连接超时异常,导致心跳上报线程中断。解决方法是把 Agent 的心跳上报设计成独立线程,并且加上异常捕获和自动重连机制——任何异常都不能让心跳线程退出。
4.2 用例执行失败率突然升高,但脚本本地跑没问题
这种问题在测试平台上线后会让人很崩溃。本地执行明明全部通过,一放到平台上跑就有几十条失败,打开日志一看全是超时、连接拒绝。
我总结的排查思路是:先怀疑环境再怀疑脚本。
- 确认执行机上是否缺少依赖包,用
pip list对比本地环境。 - 确认被测服务的地址是否不同。平台执行机很可能不在你的本机网段,无法访问 localhost。
- 确认测试数据是否存在。本地调试时数据库里可能有调试数据,新的执行机连的是另一个数据库,里面没有测试数据。
- 确认执行机时区、系统语言是否影响程序逻辑。
我们的项目就遇到过类似问题:接口用例里写死了某个环境地址,本地能通,执行机网络策略不通,导致所有依赖该地址的用例全部失败。后来我在用例表里增加了“环境标识”字段,执行时通过环境变量注入不同的 BaseURL,从根上解决了这个问题。这也是我建议所有接口测试用例把环境地址参数化的原因。
4.3 任务在队列里一直显示“等待中”,不被执行
这个问题的排查点相对集中。任务状态一直是“等待中”,说明任务没有被分发出去,最可能的原因是:没有在线且空闲的执行机。
我建议在管理页面上加一个“执行机负载”视图,直接展示每台执行机的当前任务数、最近心跳时间、状态。这样当任务“卡住”时,运维人员一眼就能看到是不是执行机队列堵住了。
另一个可能原因是轮询调度器卡在了某个网络请求上。我在 dispatcher 里所有发送任务到 Agent 的请求都加了超时时间,默认 3 秒,超过直接抛异常并记录日志,不会影响下个任务的调度。
4.4 定时任务没有触发
定时任务不触发,排查看两个点:调度器进程日志和数据库中的 cron 配置。
有次我们修改了一条 cron 规则,但调度器一直没有生效。排查后发现问题出在调度器没有定时刷新数据库的 cron 配置——APScheduler 的 job 是在内存里管理的,必须显式调用刷新逻辑才能加载新增或修改的 task。后来我在调度器里加了一个轮询数据库 cron 配置的循环,每 30 秒检查一次是否有新配置或配置变更,保证改动增量同步,这个问题就不再出现了。
4.5 报告打开速度慢
报告页面的查询慢,一般跟数据量和查询方式有关。执行记录表会随着每天定时任务执行越来越多,如果不做归档或清理,半年后查询接口可能就需要 20 秒甚至更久。
我的解决思路是:
- 执行记录表定期归档,超过 6 个月的数据转存到历史表。
- 列表页查询默认只查最近 30 天,并提供筛选条件。
- 为高频查询字段(任务编号、执行时间、状态)建联合索引。
- 报告页的统计数据单独缓存到 Redis,避免每次请求都实时聚合。
这个优化做完后,报告打开速度从原来的十几秒降到了 2 秒以内,体感提升非常明显。如果团队里有人反馈“报告开得很慢”,先排查这几个点即可。
5. 平台落地过程中的经验心得
5.1 先定流程,再写代码
很多开发者在搭建测试平台时容易从代码写起,写着写着才发现流程没定清楚:比如执行机的权限谁来管理?定时任务由谁审批?用例代码和平台数据之间的对应关系如何维护?这些问题如果没有事先想清楚,平台做到一半就很容易推翻重来。
我在项目启动阶段花了一周时间梳理流程,包括:谁可以创建用例、谁可以发起执行、执行机新增需要走什么申请流程、定时任务通知发给谁。流程定了之后,再用代码去承载它,平台才会真正贴合团队的运作方式。
5.2 用例脚本的版本管理一定要跟平台分开
我一直强调脚本仓库与平台数据分离。平台里的用例表只存元数据(名称、模块、路径),不存代码内容。代码统一放在 Git 仓库中,执行机拉取时使用固定分支。这样平台升级不影响脚本维护,脚本改动也不需要动平台数据结构。
如果想把用例代码和平台深度集成,比如在线编辑脚本、在线调试,那要投入的工程量会大很多,并且调试体验通常不如本地方便。个人建议除非是平台产品化的需求,否则没必要做在线编辑器,给测试同学提供本地开发 + 平台执行的模式就足够高效了。
5.3 平台上一定要有“可观测性”
这里的可观测性指的是:任何一个任务的状态变化,都要能在平台上查到痕迹。
我做的具体事情是给任务表加了一张操作日志表,记录任务的每次状态变更:谁创建、谁调度、分发到哪台机器、执行开始、执行结束、失败原因等。这样一旦用户反馈“任务没跑”“结果不对”,你打开日志表就能完整还原发生了什么,而不需要去执行机上看本地日志。
这个设计投入很小,但对排查问题效率的提升是巨大的。可以说,自动化测试平台里最有价值的部分往往不是炫酷的前端界面,而是那些能让问题快速定位的数据记录。
6. 后续扩展方向:从“能跑”到“好用”
平台上线稳定运行之后,我开始思考下一步的迭代方向。如果只停留在“用例能跑、报告能看”的阶段,那它本质上还不算是一个好的平台产品,只是个工具的组合。真正的平台应该能够帮助团队提升测试效率和质量。
我目前规划的扩展方向有三个:
数据驱动与关键字驱动的深化。现在的用例脚本依然需要编写代码,后续要考虑让业务测试人员通过平台低代码方式编写用例,比如通过拖拽组件配置接口请求、断言规则、参数提取等。这样能大幅降低自动化测试的参与门槛。
与 DevOps 链路深度集成。目前的定时任务还比较基础,后续可以对接持续集成流水线,在构建完成后自动触发测试任务,并将测试结果回传给流水线控制发布流程。这块的核心是提供一套稳定的开放 API,让外部系统能够灵活调用。
质量趋势分析。用例运行一段时间后,平台会积累大量历史数据。通过分析这些数据,可以统计出不同模块的失败率变化、环境稳定性指标、用例耗时排行,帮助团队定位高频失败模块,并优化测试策略。这一步需要数据仓库和可视化能力更进一步的建设。
扩展的优先级我会倾向于先把“低代码用例编辑”这块做起来,因为它能直接扩大自动化测试的覆盖面,让更多角色参与进来,而不是让自动化变成少数几个开发者的专属工具。
如果你所在的团队也正在经历测试脚本散落在个人电脑、回归需要手动触发、执行结果难以追溯的阶段,希望这篇文章能给你一些参考。搭建自动化测试平台不是一件能一蹴而就的事,但只要抓住了“用例管理、任务调度、执行分发、结果展示”这条主线,一步步迭代,它带给团队的效率提升一定值得投入。