经常有朋友拿着一堆“能跑的脚本”找我救火:昨天还可以开的自动巡检,今天突然不执行了;测试脚本在本地好好的,一到服务器就各种点位不到;定时任务看起来成功退出了,实际上关键数据一条都没处理。这类问题碰多了以后,我慢慢意识到一个事实——自动化脚本开发,真正考验人的从来不是语法或多高深的算法,而是有没有一套稳定的“最佳实践”。这里的实践不是指某个框架的用法,而是从项目结构、配置管理、日志留痕、失败重试到可持续维护的整套习惯。
这篇文章想聊的,就是我在这些年里不断踩坑、复盘后沉淀下来的自动化脚本开发方法论,主要面向刚入门想系统学习脚本开发的同学,以及正在维护大量脚本却经常被稳定性折磨的后端、测试或运维方向的朋友。内容会比较长,但基本覆盖了脚本从设计、编码到稳定运行的每个关键环节,包括Web端和App端的UI自动化常见难点。如果你愿意按这些习惯改造手上的脚本,大概率能让脚本从“今天能用”变成“长期都能用”。
1. 先给脚本分好类,再谈实践
1.1 自动化脚本的常见类型与核心难点
提到自动化脚本,很多人第一时间想到的是“Python写点小工具”,但落到实际开发中,脚本的形态差异非常大。不把这些差异搞清楚,上来套一套很火的最佳实践,往往会出现南辕北辙的效果。
从我接触过的场景看,日常开发里的自动化脚本大致能分成五类:
- 数据处理类:批量导入导出、报表统计、数据清洗、文本解析。核心难点是数据格式多变,脏数据多,很容易跑一半就崩。
- 接口调用类:定时拉取第三方接口、推送消息、同步状态。核心难点是网络不稳定、接口字段会变、鉴权复杂,需要很强的容错与重试策略。
- UI自动化类:针对Web端或App端模拟用户操作,常见于自动化测试、RPA流程。核心难点是页面DOM变化快、等待时机难把握、环境差异影响大。
- 运维操作类:服务器巡检、日志清理、批量部署、健康检查。核心难点是高危命令多,脚本必须在“无人值守”场景下足够安全和幂等。
- 校验验证类:比如自动化测试脚本、数据一致性校验,主要用于上线前检查或每日巡检。核心难点是既要能覆盖业务规则,又不能写出脆弱的断言。
这五类脚本看起来都叫“自动化脚本”,但它们的可靠性模型完全不同。写数据处理脚本,你可以容忍运行时间长一点,但必须对异常数据有明显提示;写UI自动化脚本,一旦元素找不到,光是定位和等待策略就能占掉一半开发时间。因此,最佳实践的第一步,不是急着选框架,而是先确认自己写的到底是哪一类脚本,然后把设计重心放在这一类最脆弱的环节上。
1.2 为什么大多数自动化脚本活不过三个月
我见过很多项目的脚本,上线第一周表现很亮眼,但三个月后基本成为“僵尸脚本”,要么没人跑,要么跑了没人敢信。最典型的原因有几个。
第一,没有把脚本当成长期资产来维护。很多人写脚本时的心态是“帮我把这个事自动跑一遍”,于是配置写死在代码里、日志只往控制台打、出错了也不通知人,数据输出散落在一堆变量中。一旦业务需求调整,任何人也看不明白这段脚本当初的意图,只能推翻重写。
第二,忽视运行环境的一致性。脚本在开发机上用着当前环境跑通了,换一台电脑或部署到服务器就缺包、缺路径、缺环境变量。这类问题最容易消磨维护者对脚本的信任。
第三,错误处理太粗糙。最常见的是把所有异常用一种方式“包起来”,像“try ... except Exception: pass”这种写法,看起来脚本永远在成功,实际上所有的失败都被吞掉了。等某一天数据规则变化,脚本连续失败一周却没有任何人察觉,才是最危险的。
说到底,最佳实践要解决的其实不是“怎么把代码写漂亮”这种表面问题,而是如何让脚本具备长期可运行、失败可发现、问题可排查这三项核心能力。下面的内容,我会围绕这三件事展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 写脚本前先定骨架:目录结构与技术选型
2.1 一套能撑住长期演进的目录结构
脚本项目最忌讳的是“一个大文件走天下”。早期图省事,把所有的请求、解析、通知、异常处理都堆在同一个main.py里,一旦超过几百行就基本失去可维护性。我现在写脚本,除非是二三十行的一次性小工具,否则都会按下面的骨架来组织:
text复制my_automation/
├── config/
│ ├── settings.yaml
│ └── logging.yaml
├── core/
│ ├── http_client.py
│ ├── retry.py
│ └── logger.py
├── tasks/
│ ├── order_check.py
│ ├── data_sync.py
│ └── report_task.py
├── common/
│ ├── path_utils.py
│ ├── date_utils.py
│ └── file_utils.py
├── data/
│ ├── input/
│ └── output/
├── logs/
├── tests/
├── requirements.txt
└── main.py
这套结构我用了很长时间,每次参加评审都推荐团队使用。核心思路是把不同职责拆到不同目录,让“入口”“配置”“任务实现”“基础工具”互不干扰。
config目录放所有可变参数,业务人员或者未来的维护者不需要打开代码就能调整脚本行为。core目录放复用度高的基础能力,比如统一封装的http客户端、重试装饰器、日志初始化。tasks目录放每一类具体业务,比如“订单对账”“数据同步”,它们只关心自己的业务流,不关心底层实现细节。common目录放一些纯粹的工具函数。data目录管理输入输出文件,避免脚本运行时在任意路径生成临时产物。logs目录统一收日志,方便出错时集中排查。
也许有人觉得一个日用脚本搞这么多目录很重。我的观点是:如果这个脚本预计只会运行两三次,可以直接写成一页脚本;但只要它要“定时跑”或“交给别人维护”,这层结构省下来的排查时间,很快就会远超搭建结构花掉的时间。实际项目里,我经常发现很多bug最后都是因为文件位置混乱、配置散落导致,早一点规划目录会少走很多弯路。
2.2 配置外置:把“做什么”和“怎么做”分离
目录结构之外,另一个重点习惯是配置外置。脚本代码永远只描述“怎么做”,而那些可能会变的参数,例如接口地址、开关、发送目标、时间周期、数据库连接串,全部放到配置文件中。
拿一个很常见的巡检脚本举例,配置文件可以长这样:
yaml复制app:
name: order-check
timezone: Asia/Shanghai
data:
source_db_conn: "mysql+pymysql://readonly_user:xxxx@prod-mysql.internal/order_db"
order_api_base: "https://api.example.com/v1/orders"
date_offset_days: 1
check:
retry_times: 3
connect_timeout: 5
read_timeout: 30
target_file_dir: "data/output"
notify:
email_receivers:
- oncall@example.com
webhook_url: "https://im.example.com/webhook/xxx"
为什么要费这个劲?因为脚本维护中最频繁的操作往往不是改逻辑,而是改配置。接口从测试地址切到正式地址、超时时间从10秒调到30秒、收件人邮箱变化,这些都是再普通不过的需求。参数写在代码里,你改动任何一个值都要重新发布和测试;写在配置文件里,主流程完全不用动,风险大幅下降。
我在实际操作中还喜欢把“账号密码”“密钥”这类敏感信息单独放到环境变量中读取,而不是直接写进yaml文件并提交到代码库,避免一份配置被拷贝到多个环境时泄密。配置加载很简单,用PyYAML读取后转成字典,再通过一个全局配置对象访问。不要过度设计成动态配置中心——对多数脚本场景,本地配置文件已经足够稳妥。
2.3 依赖管理:别让“换个电脑就跑不起来”成为常态
技术选型上,我的默认方案是Python,因为它做数据处理、接口调用、自动化生态都非常成熟。但Python脚本最常见的死法之一,就是依赖管理失控。今天你在自己电脑上装了一个较新版本的requests,程序跑得很好,过两礼拜部署到公司服务器时却发现服务器预装的requests是旧版本,一个参数不兼容,脚本当场GG。
对此最简单的方案是长期使用虚拟环境,并把已锁定版本的依赖清单提交到项目里:
bash复制python -m venv .venv
source .venv/bin/activate
pip install requests pyyaml pytest
pip freeze > requirements.txt
requirements.txt里每一行都会带具体版本号,比如requests==2.31.0。这样换环境时执行pip install -r requirements.txt,安装出来的依赖和开发环境完全一致,脚本运行行为才可复现。更进一步的话,可以考虑使用pip-tools或Poetry管理,但对于大多数自动化脚本而言,requirements.txt加虚拟环境已经足够。
另一个容易被忽略的原则是“减少一碰就坏的依赖面”。写脚本时请按需引入第三方库,不要因为顺手就装一堆功能重叠的包。脚本依赖越少,未来兼容性越好,这在跑在旧系统上的自动化脚本中尤其重要。
3. 可靠性设计:决定脚本生死存亡的四个细节
3.1 超时控制:给每个外部调用都装上闸门
脚本跑挂最常见的原因不是逻辑写错,而是“永远等不到回复”。我经常看到有人直接requests.get(url),不加任何超时参数,结果第三方服务宕机时,这个脚本就无限期阻塞在那里,后续任务全部排队等待。
真实的开发里,任何外部调用都必须设置超时时间。requests库可以直接指连接超时和读取超时,两者含义不太一样,连接超时是建立TCP连接的最长等待,读取超时是连接建立后等待响应的最长等待。我一般会根据场景来设置,连接超时通常2到5秒,读取超时可以放宽到10到30秒:
python复制import requests
from requests.adapters import HTTPAdapter
session = requests.Session()
session.mount(
"https://",
HTTPAdapter(pool_connections=5, pool_maxsize=10),
)
try:
resp = session.get(
"https://api.example.com/v1/orders",
headers={"Authorization": "Bearer xxxx"},
timeout=(3, 15),
)
resp.raise_for_status()
except requests.Timeout:
# 记录并进入重试或告警流程
raise
except requests.ConnectionError:
raise
这里需要注意一个实际经验:连接池参数不是随便设的。如果脚本会并发拉取很多请求,连接池太小会导致大量线程在等待空闲连接;如果连接池开得太大而目标服务能力有限,又会增加对方负载。普通串行任务直接用默认即可,只有并发量明确时才需要调大。同理,对于数据库SQL查询,也应该在连接串中或在框架层配置查询超时,不能等数据库那边永远锁住Session。
3.2 重试机制:不是所有失败都该重试
写完超时之后,另一个很容易踩坑的地方就是重试。我见过两种极端:一种是不做重试,遇到瞬时网络抖动任务直接失败,需要人工介入,拉低了脚本的稳定率;另一种是“盲目重试”,业务逻辑本身有错误,比如提交的参数签名不对,却间隔5秒又重试10次,既浪费时间又刷出大量无效请求。
根据我的经验,可重试的失败类型包括网络超时、连接错误、HTTP的5xx状态码、数据库事务锁冲突。这类失败通常是临时性的,服务恢复后再次请求很可能成功。不可重试的失败包括参数校验失败、鉴权失败、4xx状态码、业务规则拒绝和手动断言失败,这类失败无论重试多少次结果都一样,应该立刻暴露给负责人。
因此我在实践中通常会写一个“带退避的重试装饰器”,并指定哪些异常才可重试:
python复制import functools
import random
import time
import requests
def retry(max_times=3, base_delay=1, max_delay=8):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_times):
try:
return func(*args, **kwargs)
except (requests.Timeout, requests.ConnectionError, requests.HTTPError) as exc:
if attempt == max_times - 1:
raise
delay = min(max_delay, base_delay * (2 ** attempt))
delay = delay + random.uniform(0, 0.5)
time.sleep(delay)
return None
return wrapper
return decorator
@retry(max_times=3)
def fetch_orders(order_date):
# 具体拉取逻辑
pass
这里的退避策略是指数退避,第一次失败后等约1秒,第二次约2到4秒,依次递增。合理使用退避能有效减轻服务端在故障恢复后的瞬时压力,而不是一秒内重试十次。另外,如果业务场景对重复提交敏感,重试前还应确认接口是幂等的,也就是“同一请求重放多次和只放一次效果相同”,不然一次网络超时后的重试可能会产生重复订单或重复通知,造成更严重的线上事故。
3.3 日志留痕:自动化的世界里,沉默就是事故
很多自动化脚本出问题之后找不到原因,往往是因为没有日志,或者只往控制台打了几行print,进程一结束什么都没留下。运维型脚本经常在半夜无人值守时执行,假如早上发现它没有按预期产出数据,你总不希望只能靠“回忆”判断它什么时候挂了。
我自己的日志习惯是:给每个任务配置独立的日志文件,既保留控制台输出,也写入logs目录。日志记录的关键事件至少包括任务开始与结束、运行过程中加载的配置摘要、关键步骤执行结果、慢调用耗时、异常堆栈、失败上下文。代码的log内容要多到能回答一个问题:某次运行到底做了什么。
用Python自带的logging即可达到这个目标,不必引入第三方日志库:
python复制import logging
import sys
from logging.handlers import RotatingFileHandler
logger = logging.getLogger("automation")
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
console = logging.StreamHandler(sys.stdout)
console.setFormatter(formatter)
file_handler = RotatingFileHandler(
"logs/order_check.log",
maxBytes=10 * 1024 * 1024,
backupCount=5,
encoding="utf-8",
)
file_handler.setFormatter(formatter)
logger.addHandler(console)
logger.addHandler(file_handler)
RotatingFileHandler表示单个日志文件超过10MB就自动切割,并保留最近5份历史日志,避免日志文件越来越大占满磁盘。对于UI自动化脚本,我还建议把失败页面的截图文件路径写明在日志里,方便排查时快速查看现场。日志里不要记录明文密码和密钥,打码或脱敏后再写。
3.4 运行完成不等于成功:校验结果才是最终答案
这一条在我看来是脚本开发中最核心的意识。如果你只是“跑完了没有报错”,那充其量只能说明“执行了步骤”,并不代表“任务完成了预期目标”。一个典型例子:定时脚本早晨顺利退出,exit code为0,但大家一看数据库,发现昨天所有待处理文件因为编码问题被跳过了,脚本完全没处理它们。
为了规避这种情况,脚本设计初期就要明确“成功标准”。最终交付的任务,在完成主流程后还要再加一道结果校验:
- 对拉取数据的任务,校验文件非空、行数大于阈值、没有全量相同值;
- 对同步类任务,整理出受影响记录数,并与源侧的数量对比;
- 对测试类任务,断言关键步骤结果与期望一致;
- 对“扫描后处理”类任务,检查每个待处理对象都被标记为已完成,而不是被异常跳过。
代码中可以用独立的check_result函数,失败就抛异常或返回非零码:
python复制def check_result(output_path, min_rows=1):
if not os.path.exists(output_path):
raise RuntimeError("输出文件不存在")
row_count = count_lines(output_path)
if row_count < min_rows:
raise RuntimeError(f"输出行数过少: {row_count}")
logger.info("结果校验通过, 行数: %s", row_count)
这种“最终校验”意识特别适合自动化测试脚本,因为测试脚本的价值不是“代码跑通了”,而是“业务真的符合预期”。差之毫厘,脚本就可能变成一台很努力地制造错误结论的机器。
4. 实操记录:从需求到落地的完整自动化脚本过程
4.1 需求拆解:先有动作清单,再想编码
这段分享我最近手头一个真实的脚本开发过程。需求听起来并不复杂:每天凌晨两点,需要从订单平台拉取前一天的结算数据,和本地业务库中的订单表比对,最后生成一份异常订单清单并通知到相关人员。
如果直接打开编辑器开始写代码,过程中很容易漏掉细节。我习惯先做一个动作清单并思考每个环节可能的失败点:
| 环节 | 输入 | 核心动作 | 关键失败点 | 失败处理 |
|---|---|---|---|---|
| 日期计算 | 当前日期 | 算出统计日期(昨天) | 时区、跨月/跨年 | 日志记录日期 |
| 接口鉴权 | 应用配置 | 获取临时token | token过期,临时网络故障 | 过期则刷新一次 |
| 拉取数据 | token、日期 | 分页拉取本地订单 | 超时、部分页失败 | 单页重试,整体失败退出 |
| 入库对比 | 拉取数据、查询数据库 | 比对金额/状态 | 数据量差异、时间差异 | 输出差异明细 |
| 报告生成 | 差异列表 | 写Excel或CSV | 文件格式问题 | 记录异常后继续 |
| 通知发送 | 报告路径 | 发送邮箱/IM | 目标服务拒收 | 保留本地报告并可二次发送 |
这个表格不是写完就扔,它其实就是代码里主流程的草图。把失败处理想清楚后,编码会很顺,而且不容易在过程中给自己埋坑。我认为这个环节是“自动化脚本开发最佳实践”里投入产出比最高的一步。
4.2 核心代码结构的实现
基于上面的清单,我把每个环节拆成独立函数,并保持代码主线尽量简单。下面节选了主流程部分:
python复制def main():
cfg = load_config("config/settings.yaml")
today = datetime.now()
check_date = today - timedelta(days=cfg["data"]["date_offset_days"])
logger.info("开始执行订单对账 | 统计日期: %s", check_date.date())
token = request_access_token(cfg)
remote_orders = fetch_remote_orders(cfg, token, check_date)
local_orders = fetch_local_orders(cfg, check_date)
logger.info("拉取数据完成 | remote_rows=%s local_rows=%s", len(remote_orders), len(local_orders))
diffs = compare_orders(remote_orders, local_orders)
if diffs:
report_path = write_report(diffs, check_date)
notify_oncall(cfg, report_path)
else:
logger.info("不存在差异记录")
check_report_exists(diffs, cfg)
logger.info("任务执行结束")
if __name__ == "__main__":
main()
在这里我特意把数据拉取、本地数据读取、对比、报告、通知解耦成了五个函数。未来修改比对规则时,不会影响前面的拉取逻辑;要改通知方式时,也不会碰数据逻辑。对于几百行的脚本,这是性价比较高的写法。
4.3 配置与通知的落地细节
配置加载部分,我会把配置文件读成一个字典,同时处理文件名默认值和路径兼容。注意路径必须基于项目根目录计算,不能依赖“运行脚本时所在目录”,否则定时任务从别的目录执行时就会找不到配置。这里分享一个小技巧:代码里把项目根目录固定为父级目录:
python复制from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
def load_config(path: str) -> dict:
with open(BASE_DIR / path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
通知模块的落地,我的默认方案是提供一个webhook发送函数。无论是发邮件还是发IM机器人消息,基本思路都一样:把报告变成一个可访问的文件下载地址,再把文字摘要和链接发给接收人。消息里应包含任务名称、执行时间、结果是否成功、问题数量以及报告地址。写通知时注意保留原始报告的本地路径,防止webhook发送失败导致数据丢失。
5. UI自动化脚本:录制只是起点,稳定才是终点
5.1 录制生成的脚本为什么总是不稳定
现在网上有不少“UI自动化录制生成脚本”的开源项目,可以操作浏览器或手机生成测试脚本,对新手来说确实很兴奋,打开录制按钮,点几下页面,代码就自动生成了。但用久一点就会发现,录制出来的脚本有个特点:能跑通“当时那条路”,但稍微变化一点场景就崩。
录制生成脚本最常见的三大问题,我在很多项目复盘里都见过。首先是定位方式太脆弱,很多录制器会生成绝对XPath,比如/html/body/div[2]/div[3]/div/div[1]/div/div[2]/button,页面只要多一个元素或少一个元素,马上就找不到了。其次是无脑的固定等待,代码里充满了time.sleep(3),网络快慢一旦出现波动,不是空等就是还没加载出来就操作,怎么看都不稳定。最后是代码没有业务抽象,每一个用例都是“点击然后点击再点击”,没有把页面中的对象和能力抽出来复用。
换句话说,“自动化脚本开发最佳实践”对UI自动化的要求比普通脚本更高,因为它面临的变化源更多。录制生成脚本应该被当作草稿,而不是可直接上线的产物。
5.2 Page Object模式与显式等待
UI自动化的稳定方案,目前最公认的是Page Object模式。它把页面抽象成一个类,在类内部封装所有元素定位和页面交互,测试用例只关心业务行为和期望结果,不直接接触CSS选择器或XPath。我来给一个非常简化的示例:
python复制from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class LoginPage:
LOGIN_URL = "https://example.com/login"
def __init__(self, driver):
self.driver = driver
def open(self):
self.driver.get(self.LOGIN_URL)
def login(self, username, password):
WebDriverWait(self.driver, 10).until(
EC.visibility_of_element_located((By.ID, "username"))
).send_keys(username)
self.driver.find_element(By.ID, "password").send_keys(password)
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
这种封装带来的直接好处是:当登录页的输入框id从username改成userName时,只需要改LoginPage一个地方,所有调用login的用例都不受影响。如果坚持在用例里裸写定位,页面一旦调整,所有用例都要逐个排查。
时间等待上,我建议不要用固定sleep,改用显式等待:明确等待某个条件满足,比如元素可见、可点击、文本出现。显式等待通常比固定sleep更稳,也更快。移动端Appium同样支持类似的等待方式。还要记住一条铁律:断言不能和页面操作写在一起。每个Page Object的方法只做“动作”,是否成功由测试用例里的断言来判断,否则会让页面类过于脆弱,一个元素的状态稍微变化就让整个类里的其他方法也失效。
5.3 Web端与App端各自的避坑清单
Web端和App端虽都基于UI自动化,但被坑的点差异很大。
Web端最需要警惕的是iframe嵌套。很多管理系统登录后内容区在iframe里,主页面定位永远找不到内容页面元素。解决方式是先switch_to.frame跳到正确的frame再操作。其次常见的是新开标签页、浏览器原生弹窗、Shadow DOM,这些都要求脚本具备切换上下文的能力。前端项目改版频繁时,定位策略优先用稳定的语义化属性,比如id、data-testid,再不行才使用XPath。
App端的复杂度通常会更高。iOS和Android的系统弹窗、权限申请弹窗样式不同,设备连接状态也影响执行稳定性。移动端自动化还有一个常见问题——不同分辨率下元素位置会有偏差,靠坐标点触是极其脆弱,应该尽量基于可访问性ID或XPath去定位。如果使用模拟器,还要考虑模拟器本身的算力和网络环境,很多偶发性失败都来自设备过热或内存不足。
实操中,我的移动端自动化项目会强制拆分三层:测试数据层、业务操作层、设备管理层。把“连接哪台设备、安装哪个包、启动哪个Activity”这类设备能力单独封装,测试用例不操作设备命令,数据管理不绑定设备,这样多设备并行测试时才不会因资源冲突而互相干扰。
6. 自动化测试脚本开发中绕不开的工程化习惯
6.1 测试夹具与测试数据准备
自动化测试脚本其实是最讲究“可控可复现”的脚本类型。每个用例执行之前,最好能固定前置数据;执行之后,尽量清理它创建的数据,避免用例间相互污染。我在早期维护测试脚本时常常遇到“用例A创建了一个新订单,但没有清理,导致用例B在统计全量订单时数量对不上”的情况。
使用pytest框架可以用fixture机制处理测试前后置,代码可以写得很干净:
python复制import pytest
@pytest.fixture
def new_order_record():
order_id = create_order(amount=100)
yield order_id
cleanup_order(order_id)
def test_order_total(new_order_record):
total = fetch_total_amount()
assert total >= 100
在移动端或服务端的自动化里,你也可以用“初始化接口造数”代替手工构造数据。这句话很重要:能用接口造数就不要依赖准备繁琐的静态数据或人工数据。一个稳定自动化的前提是,测试开始前你非常清楚环境里有哪些数据,测试结束后又恢复成什么样子,否则你无法判断失败的用例到底是业务异常,还是数据状态异常导致的偶发失败。
6.2 报告可读性:让人一眼看出哪里出了问题
脚本跑完以后,输出的报告会影响后续排查效率。很多原始脚本只会打印一堆html标签或几十行JSON,根本没法看。我的建议是三层报告结构:
第一层是控制台摘要,脚本结束前打印总共执行了多少步骤、失败了多少条、是否有异常清单。第二层是结构化报告,例如JUnit XML、HTML报告或Excel明细,方便CI系统和自动化平台读取。第三层是失败现场的素材收集,包括错误截图、异常堆栈、拍摄日志、关键请求响应等,保存成独立文件并纳入报告。
例如pytest中可以在conftest.py里写钩子函数,用例失败时自动截图,并保存到报告目录。这样失败之后不需要重新跑一遍排查问题,看一次报告基本能快速定位是环境问题、测试数据问题还是真正的产品Bug。
6.3 无人值守时的结果通知与报警策略
自动化测试脚本绝大多数在无人值守的时间段运行,所以结果必须主动通知到对应的人。很多人刚搭好脚本时在本地跑得开心,却完全没有人关心CI里的失败结果,这是自动化落地失败的重要原因之一。
我的报警策略分为两级。失败结果汇总发到项目群,通常是文本摘要加报告链接,适合比较常见的偶发失败;连续失败次数超过阈值时再升级到人工告警,比如拨打电话或用短信触达,避免半夜一台车的设备挂了,第二天早上才发现整轮用例没跑。通知文本一定要带上环境、时间、失败的用例摘要和报告地址,否则收到告警的人还得自己去系统里找原因,告警的价值就少了一半。
7. 常见问题与排查技巧实录
任何脚本从开发到稳定运行,总免不了遇到问题。以下是我比较常碰到的情况,做成一个速查表,大家可以直接对照排查。
| 现象 | 常见原因 | 排查路径 |
|---|---|---|
| 昨天能跑今天失败 | 业务数据或时间边界变化、环境服务有调整 | 先看日志首尾,再对比成功的运行记录和失败的运行记录差异 |
| 偶发失败,重跑就正常 | 外部依赖不稳定,或者等待时机不正确 | 检查超时与重试,是否所有外部调用都有超时;查看失败尝试次数 |
| 进程退出码为0,但结果为空 | 弱校验,比如漏了非空校验 | 给数据结果增加行数、字段、完整性校验,不能只看流程跑完 |
| 脚本运行很久不退出 | 缺少超时设置,或某外部服务挂起 | 检查requests、数据库、子进程调用的timeout,必要时加总任务超时 |
| 换台机器跑不起来 | 依赖未锁定、路径写死、环境变量缺失 | 用requirements.txt锁定依赖,用项目相对路径读取文件 |
| UI脚本单条跑通过,批量跑失败 | 并发资源限制、全局状态污染、设备被占用 | 隔离测试数据,禁止用例之间共享页面状态,必要时串行执行 |
| 页面元素一会儿找得到一会儿找不到 | 网络加载波动,等待时机不符合页面实际异步过程 | 用显式等待代替固定sleep,等待条件要匹配元素实际出现时机 |
举个例子,按这个表格排查的时候,我经常看到一个现象:“进程退出码为0,但结果为空”。很多人第一反应是去查代码逻辑,但我一般先去看脚本运行结束阶段的日志有没有记录“结果校验通过”。如果没有校验日志,说明脚本根本没做结果校验。这个案例之所以普遍,本质上还是因为很多开发者在写脚本时把“异常抛没抛”当成了成功标准,而不是“业务目标是否达成”。
另外一个在UI自动化中很容易让人崩溃的情况是“元素位置变化导致XPath失效”。这种问题靠修代码往往治标不治本,页面明天又改一点,又得重写。比较靠谱的办法是推动前端团队在关键元素上增加稳定的data-testid属性,定位时优先用这些属性,测试代码就对页面样式的变动不敏感了。这一点听起来像测试团队的要求,但实际上它真正受益的是整个自动化系统的长期稳定性。
关于日志排查,还有一个小技巧:为每次运行生成一个唯一的run_id,并把这个ID同步记录到日志和输出文件的文件名中。当有人报告“脚本又失败了”,只要问一句run_id是多少,就能很快定位到对应日志和数据现场。这个习惯成本极低,却能在日常维护中节省大量沟通和排查时间。
最后再聊一点我个人的体会。从一开始只关心“代码跑通”,到后来越来越在意“失败了能否快速定位”,我确实走了一些弯路。自动化脚本开发的最佳实践,并不是某一种技术能一锤定音,而是把超时、重试、日志、校验、配置外置这一套基础好好落实。尤其是对准备长期运行、无人值守的脚本,在每一个可能失败的地方都留好痕迹,最终你收获的不仅是一个会跑的脚本,更是一套可以被团队信任的自动化系统。希望这次整理出来的经验,对那些正在折腾自动化脚本、自动化测试的人能有一点参考价值。
