ARTICLE DETAIL

资讯详情

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

自动化测试的难点不在写用例,而在异常处理工程化

自动化测试的难点不在写用例,而在异常处理工程化 自动化测试做久了你会发现一个有点反常识的现象真正让测试工程师掉头发的地方往往不是用例逻辑写不出来而是异常处理。脚本在正常路径上跑得再顺只要环境抖一下、数据脏一点、元素晚出现两秒整个自动化体系就可能开始表演“花式翻车”。这也是为什么网上那些“自动化测试框架 pytest”“Appium 自动化测试”“Java 接口自动化测试框架”相关的资料铺天盖地但真正能在实际项目里把自动化测试跑得稳定、报告敢直接甩给领导看的团队反而没那么多。我自己的感受是自动化测试的难点从来不是“把流程写出来”而是“让流程在异常条件下不那么脆弱”。这篇文章不聊那些华丽的测试理论就聊聊我这些年做自动化测试遇到的各种异常场景、处理思路以及怎么把异常处理变成一套可以复用的工程方案。1. 从“跑得通”到“靠得住”自动化测试的难点往往不在正常路径上很多人第一次接触自动化测试都会经历一个从兴奋到怀疑的过程。脚本刚写完那几天看着用例一个个绿心情特别好等跑了一个星期开始出现偶发失败点开日志发现全是些莫名其妙的原因——元素没找到、接口超时、数据被前一个用例改了。这时候你才意识到自己不是在维护测试脚本而是在维护一套需要和混乱环境长期共存的系统。1.1 自动化测试里的异常到底是什么先给“异常”这个词定个调。在编程语言里异常是程序运行时抛出的错误对象但在自动化测试的语境里我要把异常的范围拉得更宽。它至少包括以下几类代码层面的异常比如 Python 里的TimeoutException、ElementNotFoundJava 里的IOException、AssertionError。业务层面的异常接口返回了非预期状态码页面弹出了意料之外的弹窗数据写入失败但接口还是返回 200。环境层面的异常被测服务没启动、数据库连不上、测试机内存不够、移动设备熄屏。数据层面的异常上一个用例没清干净数据、测试数据被手工修改、批量初始化数据时碰到了空值。这些异常的共同点是它们不会按照你写脚本时的剧本出现。如果你在写用例时只盯着“正常路径”那脚本本质上就是个脆弱的定时炸弹——跑了 100 次可能 95 次都正常但只要碰上那 5 次异常你就会开始怀疑整个自动化测试到底有没有价值。1.2 异常处理不足的三个典型症状假绿、漏报、误报我在评审别人的测试代码时判断异常处理做得好不好通常会看三个现象是不是经常出现第一个是假绿。用例看起来通过了实际上断言根本没执行到关键点。典型的写法是定位元素失败后用了一个超长的隐式等待等到页面超时之后直接pass或者把断言写在了try块里异常被吞掉日志里什么都没留。这种“假绿”比直接报红更可怕因为它会给你一种虚假的安全感。第二个是漏报。产品真的有 bug但自动化用例没发现。常见原因包括定位元素时选了多个匹配元素里的第一个结果恰好选到了隐藏节点接口断言只校验了 HTTP 状态码没校验业务码页面加载用了固定sleep(5)5 秒后元素还在加载中脚本直接判定失败但真实原因是等待策略不合理。第三个是误报。产品是好的但用例失败了。这种误报最消耗团队信任跑十次失败三次开发就会开始无视自动化结果领导也会觉得测试团队在折腾空气。误报的根源几乎都指向同一个点异常处理没有区分“被测对象的问题”和“外部环境的问题”。1.3 异常处理在自动化测试中的本质保证结果可信所以我说异常处理的价值不是“让脚本不报错”而是让测试结果可以被信任。一个用例跑完我们需要能回答三个问题这条用例是不是真正验证了预期行为如果失败了失败原因是产品缺陷还是环境干扰下次跑的时候能不能稳定复现同样的结论这三个问题回答不了自动化测试做得再花哨也只是个昂贵的玩具。想回答好这三个问题就必须把异常处理当成核心工程来做而不是随手写几个try...except就完事。2. 拆解四类高频异常定位、数据、环境、断言我习惯把自动化测试中的异常按来源拆成四类。拆解的意义在于不同来源的异常处理策略完全不同。把代码异常、数据异常、环境异常混在一起处理写出来的代码大概率又乱又没用。2.1 定位类异常找不到、等不到、选错UI 自动化里最常见的就是定位类异常。元素没加载完、元素在 iframe 里、元素被弹窗遮挡、同类元素有多个……最后抛出来的都是“找不到元素”。这类异常的处理核心有三个等待策略、定位策略、兜底策略。等待策略上我的原则是禁用固定sleep优先使用显式等待。比如 Selenium 和 Appium 里的WebDriverWait等到元素可点击、可见、存在之后再操作。固定 sleep 不是不能用而是在等待条件无法表达时才用而且要有明确注释说明为什么等这么久。定位策略上推荐按优先级选择idname/classxpathcss selector 坐标。能写相对路径就不要写绝对路径能结合父节点和兄弟节点定位就不要只依赖一层条件。移动端 Appium 里还有ios_predicate和android_uiautomator定位方式不同平台特性要区分。兜底策略是大家最容易忽略的元素找不到以后不要立刻抛异常退出先截图、保存当前页面 DOM、记录当前 Activity 或 URL再决定是否重试。这些现场证据对后续排查定位异常至关重要。2.2 数据类异常脏数据、空值、边界值数据类异常比较隐蔽因为它在接口测试和数据驱动测试里特别常见。一个接口用例第一次跑传了个正常手机号通过第二次跑同一个用例数据库里被别的测试插了一条记录唯一索引冲突失败第三次跑输入参数里有空值后端直接返回 500。处理数据异常我常用的做法是“三分离”静态测试数据与动态生成数据分离固定不变的基础数据放配置文件每次运行需要独立的数据用代码生成。用例数据与清理逻辑绑定每条用例结束都做数据清理哪怕异常退出也要清理不然下次跑就是脏环境。数据校验与业务校验分离脚本只关心这个数据能不能支撑用例执行业务断言统一放到最后。举个例子我在做 Java 接口自动化时会在Before里准备数据、After里清理数据同时在用例内部判断关键数据是否为空为空就跳过该条用例而不是直接失败。这样能有效降低数据异常带来的误报。2.3 环境类异常依赖服务、资源冲突、状态残留环境类异常最让人头疼因为它可能不在你的代码控制范围内。常见的场景被测服务没启动接口直接连不上测试环境被人手动操作过页面状态和预期不一致移动端设备网络切换、锁屏、App 被系统杀掉并行执行用例时两个用例同时操作了同一个资源。环境异常的处理思路不是“尽量避免”而是“快速识别并做标记”。我的习惯是在异常处理里加一个“环境判断层”如果接口连不上先探活服务确实挂了就标记为环境错误error而不是用例失败failed如果是数据冲突且可以重试就自动重试两次。这个区分很重要因为测试报告的结论依赖这个分类。2.4 断言类异常信息不足导致的误判断言看似简单其实最容易出问题。很多人写断言只写一层比如接口返回的 JSON 里status是不是 1。但一个更完整的断言应该包括HTTP 状态码、业务状态码、关键字段值、响应时间、数据一致性。如果断言信息不足就会把“数据异常”误判成“产品缺陷”或者反过来。我在处理断言异常时有个习惯断言失败不要只抛一个AssertionError要在异常信息里带上期望值、实际值、请求参数、响应全文。这样排查问题时不用重新翻日志异常信息本身就已经是答案。3. pytest自动化测试框架下的异常处理实战说回 Python 生态pytest 目前是自动化测试工程师最常用的框架之一。它的异常处理能力比早期直接用unittest写要灵活得多但也需要一些设计才能用得好。3.1 预期异常用 pytest.raises别用 try...except 硬吞很多初学者处理预期异常喜欢这样写try: result api.request() assert result.status 1 except Exception: print(发生异常但我不确定是不是预期中的)这种写法的问题在于不管什么异常都被吞掉了日志里只有一行打印。正确的做法是用pytest.raises它不仅能捕获异常还能精确校验异常类型和异常信息。import pytest from requests.exceptions import Timeout def test_request_timeout(): with pytest.raises(Timeout) as exc_info: api_request(timeout0.1) assert connect in str(exc_info.value)这样写有几个好处异常类型是明确校验过的exc_info里可以拿到完整的异常上下文测试通过本身就证明了“预期异常确实发生”。在自动化测试里预期中的异常应该被当作正常业务路径来处理而不是当成错误来处理。3.2 fixture 中的异常兜底与清理机制pytest 的 fixture 是异常处理的重要载体。很多人只在用例函数里写try...except却忽略了 fixture 里的异常和清理逻辑。举个例子一个登录态的 fixture 长这样pytest.fixture def login_session(): s create_session() s.login() yield s s.close()这个 fixture 的问题是如果login()调用失败s根本没创建成功后面的s.close()会再抛一个异常把原始异常盖掉。正确的写法应该加一层兜底pytest.fixture def login_session(): s None try: s create_session() s.login() yield s except Exception: raise finally: if s is not None: try: s.close() except Exception: pass这里的关键是清理逻辑必须独立于业务逻辑不管前面执行成功还是失败清理都要执行但清理本身的异常不能影响主流程的结果。用finally包裹清理用一个空的except兜住清理时可能出现的次要异常是一种很实用的防御性写法。3.3 异常现场留痕日志、截图、系统状态的采集异常处理做到了“抛出正确异常”还不够还得把异常现场记录下来。我见过太多用例失败后日志里只有一个ElementNotFoundError没有截图、没有页面源码、没有请求日志开发只能靠猜。对于 pytest 项目我建议建立一个统一的异常钩子文件conftest.py在里面实现两个东西日志记录把异常类型、异常信息、堆栈、当前用例名、执行时间、环境信息统一写入日志。现场快照UI 测试里截图保存screenshot目录接口测试里保存请求和响应报文。一个参考实现import pytest import logging from datetime import datetime logger logging.getLogger(__name__) pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.failed: logger.error( case%s nodeid%s phase%s exception%s, item.name, item.nodeid, call.when, report.longrepr ) # 在这里添加截图/保存响应报文等逻辑有了这套机制每次失败都不是“又挂了”而是“挂在哪一步、当时环境什么样、上下文是什么”都能快速定位。3.4 重试机制设计脆弱用例的救生衣还是遮羞布关于自动化测试里的重试机制行业内争议很大。有人主张什么用例都加重试结果测试时间翻倍有人坚决不加重试结果偶发失败让团队对自动化失去信心。我的观点是重试必须有的放矢。只有可恢复的异常才适合重试比如网络抖动、第三方服务瞬时超时、移动端网络切换。而业务断言失败、数据冲突、系统崩溃这些重试多少次都不会有好结果重试只会掩盖问题。在 pytest 里可以用pytest-rerunfailures插件但更推荐自己控制重试粒度。比如在用例内部对“可重试操作”单独做成函数def retry_call(func, retries3, interval2, exceptions(TimeoutError, ConnectionError)): for i in range(retries): try: return func() except exceptions: if i retries - 1: raise time.sleep(interval)这样重试不是骗自己而是把一个已知的、偶发的环境问题当成预期干扰处理完之后再继续真实断言。重试次数、间隔、哪些异常可以重试都要作为参数配置而不是写死。4. Appium自动化测试的移动端异常处理等待、弹窗、崩溃移动端的自动化测试比 Web 端更麻烦因为涉及到设备状态、系统弹窗、应用生命周期等一大堆变量。Appium 作为最主流的移动端自动化工具异常处理的设计思路和 Web 端有不少差异。4.1 元素定位超时的等待策略设计Appium 里最经典的问题就是“明明元素在页面上脚本就是找不到”。移动端的渲染机制、网络延迟、动画过渡都会导致元素出现的时机不可控。只设置一个全局implicitly_wait根本不够因为不同页面的加载时间差距很大。我自己的方式是全局设置一个较大的隐式等待比如 10 秒负责兜底关键操作使用WebDriverWaitexpected_conditions并且设置不同业务的等待阈值尽量避免用sleep除非是等待动画结束这种显式等待表达不了的场景。一个比较规范的写法from appium.webdriver.common.appiumby import AppiumBy from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_element(driver, locator, timeout15): return WebDriverWait(driver, timeout).until( EC.presence_of_element_located(locator) )如果超时不急着抛异常先截一张图再把当前页面的 page source 存下来。很多移动端定位问题看截图和 page source 就明白了。4.2 系统级弹窗与页面状态切换的兼容处理移动端自动化里“弹窗”是个高发异常源。iOS 和 Android 的权限弹窗、更新弹窗、广告弹窗、崩溃恢复弹窗它们可能在任何时刻出现把你的点击目标挡住。处理这类异常我见过的有效做法是做一个“弹窗清理器”在每次关键操作前后尝试检测是否存在常见系统弹窗存在就自动点掉。比如 Android 上处理权限弹窗可以按 text 定位iOS 上处理系统弹窗可以用XCUIElementTypeAlert。这里要注意不要对“是不是弹窗”做太强的假设否则容易把正常页面上的按钮误点掉。更稳妥的方式是只处理已知的、有限集合的弹窗类型其余的情况交给截图和人工判断。4.3 崩溃检测与关键证据保存Appium 里有一个经常被忽略的异常场景App 崩溃。当 App 闪退时driver 可能还能继续执行但页面已经回到桌面后续的定位操作会全部失败。这时候脚本报的错误是“元素找不到”而不是“App 崩溃了”很容易让排查方向跑偏。我建议在用例的关键节点检查 App 状态。Android 上可以通过driver.current_package判断是否还在目标包名内iOS 上可以检查driver.current_context或尝试找系统元素。如果发现 App 不在前台立即记录崩溃现场取系统日志Android 的logcatiOS 的 system log保存截图标记该用例为“崩溃导致失败”而不是普通的断言失败。这样做的好处是开发拿到报告后第一眼就知道问题出在 App 崩溃而非用例逻辑处理速度会快很多。5. Java接口自动化测试框架中的异常体系设计Java 在接口自动化领域依然有很重的分量尤其是企业级项目里。Java 的异常处理比 Python 更强调体系化这点在自动化测试里其实非常适用。5.1 接口异常的三个层次网络、协议、业务做接口自动化时我一直把异常分成三层来设计网络层SocketTimeoutException、ConnectException这是最底层的连接问题。协议层HTTP 状态码不是 2xx但响应还是合法的 HTTP 报文。业务层HTTP 200 但业务码不是预期值或者字段缺失、数据不一致。这三层在代码里的体现应该分别对应不同的异常类型。所以我倾向于在测试框架内部自定义异常体系而不是把所有异常都混在一个Exception里。举例来说public class NetworkException extends RuntimeException { public NetworkException(String message, Throwable cause) { super(message, cause); } } public class ProtocolException extends RuntimeException { private int statusCode; public ProtocolException(int statusCode, String responseBody) { ... } } public class BusinessException extends RuntimeException { private int bizCode; public BusinessException(int bizCode, String message) { ... } }有了这三层异常测试代码里就能分别处理网络层可以重试协议层可以记录响应体业务层直接决定用例成败。这种设计在维护大型接口测试集时特别舒服因为哪个环节出错一眼就能看出来。5.2 自定义异常结构和统一断言处理Java 接口自动化中断言处理很容易写得散。每个用例都写一串assert或Assert.assertEquals一旦测试集变大想统一处理断言失败就很难。我更推荐给断言封装一个统一的方法。比如说响应校验我把常见的校验规则收敛到一个工具类里public static void assertBizSuccess(BaseResponse response) { if (response null) { throw new NetworkException(响应为空可能服务未启动, null); } if (!response.isHttpOk()) { throw new ProtocolException(response.getStatusCode(), response.getRawBody()); } if (!response.isBizOk()) { throw new BusinessException(response.getBizCode(), response.getBizMessage()); } }所有用例只需要调用assertBizSuccess(response)异常会被自动分类报告里也能正确区分失败类型。这样还有一个额外的好处如果产品改了业务码规范只需要改这一个统一方法不用满世界找断言。5.3 从异常分类到测试报告UDS自动化测试报告中的状态逻辑聊到测试报告自动化测试领域的另一个热搜话题就是“UDS 自动化测试输出测试报告”。UDS统一诊断服务是汽车电子领域常用的诊断协议UDS 自动化测试一般指针对诊断服务做自动化验证。这类测试对报告的要求极苛刻因为汽车电子测试报告是要过评审的状态分类不清晰工程师根本不敢签字。我的理解是UDS 自动化测试的报告逻辑跟普通接口测试有一个共同点异常必须分类而不是只分“通过”和“失败”。一份合格的自动化测试报告至少要区分四种状态PASS断言全部通过测试目标达成。FAIL产品行为与预期不一致属于缺陷。ERROR测试执行过程中环境异常比如诊断仪连接失败、ECU 无响应、报文超时。SKIP前置条件不满足用例被跳过。在 Java 框架里这正好对应此前设计的异常体系。网络层异常记为ERROR协议层和业务层异常记为FAIL。报告里如果把ERROR算进失败率里数字会失真如果把FAIL说成ERROR缺陷就会被掩盖。异常处理和报告逻辑是绑在一起的处理不好异常报告就是一堆不可信的数字。6. 从 AI 自动化测试和面试中的经典追问看异常处理的进阶方向这个话题最近几年越来越多人聊AI 自动化测试也确实在改变一些东西。我想聊聊它对异常处理可能带来的变化以及面试官最喜欢问的那些点。6.1 AI 自动化测试会改变异常处理的方式吗AI 自动化测试目前有两条线比较热一是基于视觉的 UI 测试直接用图像识别代替元素定位二是自愈式测试脚本AI 根据页面变化自动修复定位器。这两种方式对异常处理的影响是正面的。视觉定位天然对页面结构变化不那么敏感可以降低“定位类异常”的概率自愈式脚本则可以自动处理一些原本要靠人手工改代码的异常。但我要泼一点冷水AI 解决的是“已知异常的自动化响应”它替代不了“异常分类和决策逻辑”。哪怕 AI 自动修复了定位器你依然需要判断这次修复是不是真的对是不是引入了新的误判。所以 AI 自动化测试不会让异常处理变得不重要反而会让异常处理变成更核心的工程质量控制点。6.2 面试题里的异常处理考点该如何回答“自动化测试面试题”这个热搜词背后是很多准备跳槽的朋友在刷题。其实面试官问异常处理通常不是要你背 API而是想验证你有没有真正在项目里解决过问题。常见的追问有以下几类你如何区分产品缺陷和环境问题我会答从异常分类出发业务断言失败属于缺陷连接超时、服务未启动属于环境问题通过自定义异常体系在报告里区分FAIL和ERROR。用例偶发失败怎么排查我会答先看失败现场截图、日志、响应报文再分析失败步骤的等待策略和数据依赖最后决定是修脚本还是加重试。重试机制会不会掩盖 bug我会答会所以重试只适用于网络抖动这类可恢复异常业务断言失败绝不重试。你用什么方式保证测试报告可信我会答统一异常分类、统一断言处理、失败现场留痕、报告状态分类。这些问题的答案其实都是同一个思路在不同场景下的体现。如果让大家总结那就是一句话异常处理不是为了消灭异常而是为了让每一次异常都有明确的分类和可追溯的证据。7. 写在最后我在异常处理上踩过的一些坑最后聊点我的私货。自动化测试做了这么多年我在异常处理上踩过的坑不算少。第一个坑是“过度处理”。有一段时间我为了让用例在任何一个异常面前都不崩到处加try...except和重试结果所有用例都稳定变绿了但实际上是很多断言被异常处理吞掉用例根本没执行到真正的校验点。那段时间测试报告全是假绿还害得一个上线缺陷差点滑过去。从此我给自己定了一个规矩每个except分支必须回答三个问题——这个异常是预期的吗为什么需要捕获捕获之后要做什么回答不出来就不许加这个分支。第二个坑是“日志裸奔”。早期写的自动化脚本异常时只打印一行“error occurred”没有上下文。后来线上出了事故靠日志定位问题花了整整一天。现在我要求团队的测试脚本里所有异常信息至少包含发生异常时执行的是哪一步、输入参数是什么、期望值是多少、实际值是多少、有没有截图或报文。这个习惯带来的收益是立竿见影的。第三个坑是“重试一把梭”。有些团队为了追求通过率所有用例一律重试 3 次最后通过率是好看但测试时长翻了一倍而且真的缺陷被重试“冲”掉的概率也上来了。我现在的策略是只在明确可恢复的异常类型上重试并且重试次数记录在报告里。重试本身不应该是一个隐藏动作它必须对看报告的人可见。说了这么多其实核心观点很简单自动化测试不是“写脚本”而是“构建一套可信任的验证系统”。异常处理、日志、报告、重试、异常分类这些都是构建信任的砖石。别小看这些细节它们才是真正决定一套自动化测试能不能在团队里长期跑下去的底气。
返回列表