ARTICLE DETAIL

资讯详情

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

自动化测试进阶函数:提升脚本稳定性与可维护性的关键

自动化测试进阶函数:提升脚本稳定性与可维护性的关键 写软件测试这块有一点年纪的朋友应该都有体会我们最初做自动化测试基本都是从“背函数”开始的——find_element点什么、send_keys输什么、click点什么背熟一套就感觉已经会自动化了。等真正进了项目、跑了两轮回归之后才发现函数谁都会调拉开差距的是你怎么组合它们、怎么处理等待和异常、怎么让用例在业务变动时少改几行。这篇作为“软件测试知识点总结”系列里自动化测试常用函数的第二篇重点跟上篇做个区分上篇讲的是必备基础函数这篇讲的是能直接提升脚本质量和运行稳定性的进阶函数覆盖 Web UI 自动化和接口自动化两条主流路线。适合已经能独立写简单自动化脚本、正在往框架设计和工程化方向走的同学也适合准备自动化测试面试、想在深挖底层原理时有点谈资的人。我先把结论放在前面如果你只是想跑通一条用例那常用函数确实只有那么二三十个但如果你想让这套用例能长期跑、能在团队里推广、能让别人接手时不骂人那么真正值得反复研究的函数其实集中在这几个点上——显式等待条件和断言机制、参数化与数据绑定、fixture 的处理、请求封装与重试策略。接下来我按实际使用频率和重要性逐层拆。1. 自动化测试函数体系整体拆解别急着背函数先搞清楚函数在测试里承担的角色1.1 自动化测试的“函数”到底分几层很多初学者拿到一个自动化测试项目第一反应是打开源码找测试用例文件看到一堆函数定义然后逐个去背。这种做法效率低而且容易陷入“背了忘、忘了背”的循环。实际在工程层面自动化测试里的函数基本可以按层级分成四块理解了这四块你就抓住了主线。第一块是“驱动层函数”。这一层负责跟浏览器、App 或接口客户端打交道典型代表是 Selenium 里的webdriver.Chrome()、driver.get()、driver.find_element()Appium 里的driver.implicitly_wait()、driver.find_element_by_id()接口测试里 requests 库的requests.get()、requests.post()。这一层的特点是它们是自动化测试最底层的“手和脚”几乎不能替代但也是最不值得花大量精力去背的部分——因为它们的用法你用到哪查到哪就行真正重要的是理解它们的执行逻辑和参数含义。第二块是“业务封装函数”。这一层是把重复的底层操作提炼成有业务语义的自定义函数比如login(user, password)、add_to_cart(item_id)、create_order(order_data)。这一层是测试代码质量的试金石封装得好的项目用例层读起来几乎像在描述业务操作封装得差的项目用例层全是driver.find_element(By.XPATH, //div[contains(class,...)]).click()这种细节噪音。第三块是“断言与校验函数”。这是自动化测试的灵魂。很多同学用例跑得欢但断言只会写assert 1 1这种假断言或者干脆不写断言。这一块最典型的是 pytest 的assert表达式 pytest.approx()做浮点数比对以及自定义的断言集合模块。第四块是“框架支撑函数”。比如 pytest 的pytest.fixture、pytest.mark.parametrize、pytest_runtest_makereport钩子函数allure.step()、allure.attach()这类报告增强函数。它们是让测试用例能组织起来、能产生有效报告、能灵活扩展的“底层架构”。1.2 为什么同样是点击操作有人写成 5 行有人封装成 1 行我见过很多刚入行的同学写脚本每个用例里面都要来一遍“查找元素 等待 点击”一个用例 80 行其中 70 行都是找元素的细枝末节。真正的问题不是他们不会写而是没有建立“函数即抽象”的思维。举个很简单的例子同样是点击某个按钮我刚接触自动化的时候写的是driver.find_element(By.ID, login-btn).click()后来项目复杂了这个按钮经常因为页面渲染慢而点不上于是改成了WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, login-btn)) ).click()再后来很多页面都有这个点击需求这段代码我复制了十几份。直到有一次前端把按钮的 id 改成了login-submit我花了整个下午去改那十几个地方。从那天起我再也不这么写了直接做了个click_element(locator, timeout10)函数把显式等待和点击动作收进去业务层只传定位符。这样改一个逻辑只动一个地方这正是函数封装的核心意义消除重复、统一变化点、提升可维护性。理解这一点再看任何函数你的眼光都会不一样——你关心的不是这个函数本身怎么写而是它放到哪一层、要解决哪种变化。2. 进阶函数逐个拆Web UI 自动化里的这几个函数决定了脚本寿命2.1 元素等待函数隐式等待和强制等待为什么会被显式等待取代先说结论请把代码里的time.sleep()当成万不得已的手段把implicitly_wait当成基础配置把WebDriverWaitexpected_conditions当成主力等待方式。time.sleep(3)这种强制等待的罪过在于它不管页面 0.5 秒就加载完了还是 10 秒还没出来一律傻等 3 秒。页面快了它浪费时间页面慢了它照样报错。我见过一个项目100 条用例平均每条里有五六个 sleep跑一轮全量要一个半小时。后来把 sleep 换成智能等待时间压缩到 40 分钟还更稳定了。implicitly_wait(10)是 Selenium 提供的隐式等待它设置了一个全局的超时时间每次find_element找不到元素时轮询等待直到超时。它的问题也很明显这个等待只作用于元素查找判断不了元素是否可见、是否可点击而且它是全局的一旦某个页面元素加载特别慢或者某个元素压根不存在所有查找都要干等满 10 秒。真正靠谱的是显式等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10, poll_frequency0.5) # 等待元素可见 wait.until(EC.visibility_of_element_located((By.ID, success-tip))) # 等待元素可点击 wait.until(EC.element_to_be_clickable((By.XPATH, //button[contains(text(),提交)]))) # 等待元素消失比如 loading 遮罩层 wait.until(EC.invisibility_of_element_located((By.CLASS_NAME, loading-mask)))注意这里By.ID这种定位方式一定要套在括号里传进去expected_conditions里绝大多数条件接收的是 locator 元组而不是裸的定位字符串这是新手最容易踩的坑。2.2 expected_conditions 里最常用的几个条件和函数封装技巧expected_conditions本身是一个模块里面提供了大量“等待条件”它们本质上都是一些定义了__call__方法的类。你用的时候既可以直接用WebDriverWait(...).until(EC.xxx)也可以把EC.xxx理解为一个“带状态的条件函数”。我实际项目里最常用的条件有这么几个条件适用场景返回值visibility_of_element_located等待元素出现在页面且可见元素对象element_to_be_clickable等待元素可见且可点击元素对象presence_of_element_located等待元素出现在 DOM 中不要求可见元素对象invisibility_of_element_located等待元素隐藏或消失布尔值text_to_be_present_in_element等待元素文本变为期望值布尔值url_contains等待页面 URL 变化比如跳转后布尔值这里要特别提醒visibility_of_element_located和presence_of_element_located很多人混着用但两个判断逻辑差异很大。“presence”只要求元素在 DOM 树里存在就算它宽高为 0、完全不可见也会返回“visibility”则要求元素不仅存在还被用户看得到。有的场景里你用presence去取元素然后执行click()大概率会碰上“元素不可交互”的异常因为元素可能还在渲染。基于这些条件我自己的做法是封装一小组“等什么就函数名叫什么”的方法比如等元素可见并返回元素、等元素可点击并完成点击、等文本出现并返回文本内容核心是让上层用例读起来干净。这个习惯在我带团队、做 code review 的时候明显降低了很多沟通成本。2.3 断言函数不是 assert 那么简单pytest 断言和软断言的取舍UI 自动化里最常见的毛病是断言要么太多、要么太少。太多是指每个操作后面都跟一大段校验把用例写成了状态检查器太少是指只在最后面草草断言一句“页面跳转成功”。我的标准是“每个用户可见的核心变化都值得一个断言但不要断言无关的中间过程。”pytest 的assert本身就很好用失败时能自动展开表达式细节。比如assert login_success_text 欢迎回来失败时pytest 会把左右两个值的具体内容打印出来。这一点比unittest的assertEqual直观太多。不过纯assert在“一个步骤后要验多个点”的场景下有个问题第一条断言挂了后面的断言直接跳过你得反复跑才能把所有问题点收集齐。解决方式是用软断言比如pytest-assume插件from pytest import assume def test_login_flow(): assume(check_element_exist(user_name)) assume(check_button_clickable(submit_btn)) assume(get_text(login_result) 登录成功)这样即使第一个assume失败后面的也会继续执行一次跑完能拿到全部失败点。代价是软断言失败时不会立刻中断后面代码如果依赖前面的结果需要你自己控制好逻辑。我的建议是核心连续流程用普通assert多个独立展示点的校验用assume按场景选。另外一个很容易忽略的断言点是 UI 自动化里的“元素不存在”怎么断言。很多人会写成assert not driver.find_element(...)这非常危险——元素找不到时find_element会抛异常于是你的断言永远不会走到“不存在”这条分支。正确写法是用find_elements判断列表长度assert len(driver.find_elements(By.CLASS_NAME, error-message)) 0这个思路在做表单校验类用例时几乎是必备技能。3. 接口自动化里的常用函数参数化和数据绑定是灵魂3.1 requests 核心函数和 session 管理接口自动化测试虽然不属于 UI 范畴但自动化测试的系统知识里它一定是重头戏。最常用的请求库是requests它的核心函数就三类get/post/put/delete发请求Session管理会话Response对象上的一系列属性方法。很多人用requests.get()一条一条地发请求这种写法在单接口调试时没问题但做业务流程串联时会有个坑cookie 和鉴权状态不共享。比如你先登录拿到 token再下一个请求要带着这个 token如果用裸函数你得手动把 token 提取出来塞到下一个请求的 header 里代码又杂又容易遗漏。而用Session就不一样了import requests session requests.Session() def login_and_get_session(host, username, password): login_url f{host}/api/login payload {username: username, password: password} response session.post(login_url, jsonpayload) # 服务端返回 token 时自己存到 session 的 headers 里 token response.json().get(data, {}).get(token) if token: session.headers.update({Authorization: fBearer {token}}) return session后面所有接口都通过这个session对象调用鉴权信息自动带上。这就是“函数复用”在接口层的具体体现——你可以把登录逻辑、token 刷新逻辑、请求头拼装逻辑全部封装成函数用例层只关心业务参数。Response对象上的常用函数也得心里有数response.json()解析 JSON 响应体response.status_code看状态码response.text拿原始文本response.headers看响应头。碰到返回大报文、需要校验某个字段是否在多层嵌套结构里存在时提前封装一个get_json_value_by_path(data, data.order.items.0.price)这种按路径取值的函数能在写用例时省下大量重复的[data][order][items]切片操作。3.2 pytest 参数化函数一条用例跑出多个数据集接口测试里最核心的测试设计技巧就是数据驱动而数据驱动在 pytest 里的落地方式就是pytest.mark.parametrize。这个装饰器函数的威力在于它能把一组测试数据循环传给同一个测试函数生成多条独立测试用例每一条失败都能单独定位。它的基础用法是这样import pytest pytest.mark.parametrize(case, [ {name: 正常登录, username: admin, password: 123456, expect_code: 200}, {name: 密码错误, username: admin, password: wrong, expect_code: 401}, {name: 用户不存在, username: nobody, password: 123456, expect_code: 404}, ]) def test_login_api(case): response do_login(case[username], case[password]) assert response.status_code case[expect_code]这里有三个细节值得展开第一参数化数据和用例逻辑要分离。上面这种直接把列表写在装饰器里适合数据量小、跟用例强相关的情况数据量大了之后更推荐把数据放到独立的 JSON/YAML 文件里用工厂函数读取并返回这样业务人员也能维护数据。第二参数化用例的失败信息要能一眼定位是哪组数据挂了。pytest 默认会在用例名后追加参数值作为 ID但碰到 dict 类型参数时显示的内容很丑。解决办法是用ids参数手动指定每条用例的名称pytest.mark.parametrize(case, case_data, idslambda case: case[name])这样测试报告里显示的是“正常登录”“密码错误”这些用例名谁挂了一目了然。第三参数化同样适用于 UI 自动化但要注意把定位符也作为参数传递时最好给定位符包一层元组避免 pytest 的字符串格式化出问题。这是我在pytest.mark.parametrize里传 locator 时踩过的一个小坑后面常见问题里再细说。3.3 allure 相关函数报告是测试的“另一面”测试报告这块在“函数”语境下容易被忽略但实际你写自动化测试这个动作本质上是给项目建立信心体系如果结果无法被有效展示和解读信心的建立就打了折扣。allure在 pytest 里有一组装饰器和函数值得熟练allure.story()、allure.feature()做用例分层allure.step()做步骤展示allure.attach()做现场数据截图。我在 UI 自动化里几乎是强制要求在异常时截图并附加到报告里import allure def shot_on_failure(driver, namefailure_screen): try: screenshot driver.get_screenshot_as_png() allure.attach(screenshot, namename, attachment_typeallure.attachment_type.PNG) except Exception: pass这个函数的价值在于用例失败后报告里直接能看到体面的截图再也不用在回忆里猜当时页面长什么样子。接口自动化里也可以用allure.attach(response.text, response_body, allure.attachment_type.TEXT)方便追踪失败请求的返回内容。4. 实操过程把常用函数组合成一套可维护的测试框架4.1 fixture 函数前置后置处理的现代写法前面提到的都是具体功能的函数现在聊一个“框架级”的函数体系——pytest.fixture。很多从unittest时代过来的人习惯在类里写setUp和tearDown但 pytest 的 fixture 函数灵活得多它的核心价值是依赖注入和按需使用。看一个最简单的登录 fixtureimport pytest from api_client import TestClient pytest.fixture(scopeclass) def client(): c TestClient() c.login(admin, 123456) yield c c.close()这个clientfixture 一个类里只执行一次测试类的方法只要声明参数clientpytest 就会自动把函数返回值注入进来。你不用在每个测试方法里去调用“构造登录”——依赖是声明式获得的。yield前是前置逻辑yield后是后置清理逻辑一个函数搞定 setup 和 teardown。fixture 的scope参数决定了生命周期function每个用例跑一次class每个类跑一次module每个模块跑一次session整个测试会话跑一次。合理设置能大幅提升执行效率。我在实际项目里长期踩过的一个坑是把一个可变对象作为 fixture 返回值比如一个 dict 或者 list。如果某个用例修改了这个对象又没还原下一个用例拿到的就是“脏数据”。这种问题排查起来很隐蔽因为代码看着完全没问题。解决办法是尽量让 fixture 返回“工厂函数”而不是直接返回对象实例或者用yield明确之后让 pytest 做清理。4.2 数据驱动和测试用例生成函数把测试用例数据外部化之后还需要一个读取数据的“用例生成函数”。这里我常用一个组合先写一个load_test_data(file_path)通用函数统一读 JSON/YAML再让参数化装饰器读取它。import json import pytest def load_test_data(file_path): with open(file_path, r, encodingutf-8) as f: return json.load(f) login_cases load_test_data(./data/login_cases.json) pytest.mark.parametrize(case, login_cases, idslambda c: c[name]) def test_login(case): ...这个方案的好处是测试数据和测试代码物理隔离。接口 URL 变了、字段新增了通常只需要改数据文件测试函数不需要跟着动。从团队协作角度测试数据可以交给业务分析师或测试设计人员维护开发人员只需要保证函数解析规则稳定。这里强烈建议数据文件里有一层“标准化的字段约束”比如每条数据必须包含name、request、expect三个字段expect里可以包含status_code、contains_text、db_check等不同类型的验证点。这样你的通用执行函数才能统一解析。4.3 重试机制让用例从“偶发性失败”里站起来自动化测试到了成熟阶段最大的烦恼不是“测不出来”而是“偶发性失败”。网络抖动、页面渲染慢、第三方服务超时都可能导致一条用例这轮跑挂了、下轮又过了。这类问题如果不处理测试报告的可信度会被严重消耗。pytest 生态里有一个pytest-rerunfailures插件它提供pytest.mark.flaky(reruns3, reruns_delay2)这个函数装饰器pytest.mark.flaky(reruns3, reruns_delay2) def test_user_create(): ...这条用例失败后会自动重跑最多 3 次每次间隔 2 秒。用法上要克制重试只适合覆盖“偶发环境问题”不适合掩盖“确定性业务 bug”。如果一条用例连续稳定失败你加了重试只会延长任务执行时间还可能让真正的回归问题淹没在整体通过的假象里。我自己的策略是两层接口层的关键用例不开重试保证失败立刻暴露UI 层对“前置页面加载耗时大”的用例开低次数的重试并且重试之前先截图日志现场避免排查问题的时候丢失第一手现场资料。4.4 一个完整的 UI 自动化用例长什么样把前面说的函数组合在一起你得到的测试用例不再是一长串底层 API 调用而是这样一段像业务剧本一样的东西import pytest from pages.login_page import LoginPage from pages.order_page import OrderPage pytest.mark.parametrize(product_id, quantity, [(P1001, 1), (P1002, 3)], ids[p1001, p1002]) def test_add_order(product_id, quantity, login_env): order_page OrderPage(login_env) order_page.add(product_id, quantity) assert order_page.get_amount_text() expected_amount(product_id, quantity) assert len(order_page.get_order_items()) quantity 1用例层只关心“做什么”和“验证什么”底层函数负责“怎么做”。这种分层以后面对前端小改动大多数情况只需要动 Page 对象或者数据文件不用动用例本身。这也是为什么我说常用函数的真正价值不在“会用”而在“用在正确的位置”。5. 常见问题与排查技巧实录5.1 等不到元素先分清是等待不够还是定位不准UI 自动化报TimeoutException是家常便饭。很多新人的第一反应是调大WebDriverWait的超时时间但实际排查我建议按这个顺序走第一步打开浏览器手动复现确认元素是不是真的存在。如果页面根本不存在你定位的元素那等多久都没用问题出在定位表达式。第二步确认元素存在但不唯一。用find_elements查一下匹配数量如果一个表达式匹配到了多个元素而脚本恰好不在第一个上极大概率会行为异常。第三步确认元素存在且唯一但仍超时重点观察元素是不是在 iframe 里、是不是在 Shadow DOM 里、是不是需要先展开下拉面板。这三个也是最容易忽略的“等不到”原因。排查工具上我一般直接用浏览器开发者工具验证定位表达式用$$()在 Console 里跑一下看匹配效果能省非常多反复跑脚本的时间。5.2 参数化数据报错排查从“用例 ID 混乱”下手一个高频翻车点是参数化数据量一大报错之后你根本看不出是第几条数据挂了。这时ids参数和用例命名机制就能救你一命。给每条用例起一个有意义的名字报错信息里直接显示“test_login_cases密码错误”这种明确标识你连日志都不用翻就知道挂了哪一条。另一个更隐蔽的坑是参数化传入元组形式的 locator比如(id, login-btn)但 pytest 的参数列表解析和 selenium 的 locator 格式偶尔会混在一起导致WebDriverWait.until接收到一个字符串而不是元组。我的经验是参数化传 locator 时统一使用By.ID格式的定位对象并在测试函数入口处加一个类型检查断言assert isinstance(locator, tuple) and len(locator) 2别觉得这多余实际排查时它能帮你快速区分是“数据问题”还是“框架问题”。5.3 接口测试中 Session 状态串用例接口测试用session时最容易出的问题就是用例间的状态污染。某个业务用例为了测试故意删除了一个用户结果另一个用例正好要拿这个用户下单两条用例一起挂。这种灾难的根源是测试用例之间隐式共享了可变状态。我的做法是每个测试类用一个独立session并且对于“会产生副作用”的测试数据在 fixture 的后置阶段做数据清理而不是依赖用例自己处理。清理逻辑本身也要封装成函数比如delete_user(user_id)、reset_order_data(order_no)。测试能跑得再快也没有稳定重要。5.4 常见问题速查表问题现象常见原因排查与解决NoSuchElementException定位表达式错误或元素在 iframe 内DevTools 验证表达式检查 iframe 切换ElementNotInteractableException元素存在但被遮挡或未完全渲染改用element_to_be_clickable等待StaleElementReferenceException页面刷新后旧元素对象失效重新定位元素避免变量长期持有元素用例偶发失败外部接口响应慢、前端渲染抖动合理使用 flaky 重试先截现场日志接口响应体里取不到字段JSON 结构嵌套多层或者值在 list 里封装路径取值函数先打印完整 JSON参数化用例名全是表达式没有配置ids使用idslambda c: c[name]断言失败后报告缺少现场没有挂 allure 截图钩子在 hook 里调用截图函数 attach 到报告5.5 一个容易忽视的细节日志函数也是测试函数的一部分这算是我个人的一个执念。很多人觉得日志不属于“测试函数”但我在实践里发现80% 的用例排查效率提升来自日志。建议封装一个简单的logger工具函数在关键步骤打印操作对象、参数、响应状态码和耗时宁可多打几步也不要让“查日志”变成“翻代码猜逻辑”。import logging import time logging.basicConfig(levellogging.INFO, format%(asctime)s %(levelname)s %(message)s) def step_log(msg): logging.info(f[{time.strftime(%H:%M:%S)}] {msg})在每个核心动作函数里埋入这种日志跑完一轮测试后你按时间线就能完整还原每一步发生了什么。这对排查“为什么前面成功后面失败”的问题帮助很大。记录一些经验感受写了这些年自动化最大的体会是自动化测试的函数从来不是孤立存在的它们组合在一起才构成测试能力。你背会了find_element、click、send_keys、assert这只能保证你“手上有活”只有真正理解等待机制、参数化、fixture、重试和日志之后你才能保证测试用例在复杂项目里“活得久、跑得稳、报得清”。如果你正处在“函数都会写但用例总不稳定”的阶段我的建议是先别急着扩充新函数而是把你已经用的那批函数重新审视一遍哪些可以用显式等待替换 sleep、哪些断言可以更精准、哪些数据可以抽出来参数化。把这一步踏踏实实做完比你盲目追求再多新函数都有用得多。
返回列表