1. 从“会写脚本”到“会写函数”为什么自动化测试绕不开这一层1.1 函数是自动化用例的“积木”很多刚入行的朋友写自动化测试习惯是“一个用例一个脚本”把所有步骤从上到下堆在一起。比如想测登录就打开浏览器、输入用户名、输入密码、点登录、断言页面跳转看起来很直接但第二个用例要复用这些步骤时就只能复制粘贴。等到第三个、第四个用例加进来脚本里到处是重复代码改一个元素定位就要全局搜索替换维护成本翻着倍往上涨。函数在这里的作用本质上就是把重复动作打包成可复用的积木。我经常跟新人打比方写自动化用例就像做菜函数是提前调好的预制调料包。你做番茄炒蛋和蛋炒饭盐、糖、生抽的用量几乎一样与其每次现拿三个瓶子各倒一遍不如提前按配比装成一小包炒菜的时候拆开倒进去就行。函数也是这个道理把“输入用户名”“点击按钮”“等待元素出现”这类动作封装好用例本身只需要关心业务顺序。从软件测试的角度看自动化脚本不是写给自己看的它要长期运行、多人维护、随时排查问题。如果每个用例都是独立的“一大坨”那它既是回归测试的定时炸弹也是团队协作的噩梦。而封装函数的收益非常直接第一复用逻辑减少重复代码第二统一修改入口定位变了只改一处第三用例读起来像业务描述别人能看懂你在测什么。1.2 自动化测试函数的分类地图自动化测试里的常用函数按用途可以分五类分类核心作用典型函数/方法定位操作类找元素、做交互find_element、click、send_keys、select等待类同步页面状态WebDriverWait、time.sleep、locator.wait_for数据准备类生成、读取、清理数据faker、json.loads、openpyxl、random断言类验证结果assert、allure.assert、pytest.raises工程辅助类日志、报告、截图、重试logging、allure.attach、pytest.fixture这篇文章是系列第5篇重点讲自动化测试常用函数的第二弹我会先从Web自动化的高频操作函数和等待函数讲起然后重点拆解接口自动化里requests和pytest配合时的辅助函数再说数据驱动相关的读取处理和参数化组装最后用一整章讲我实际排查过程中的高频报错和面试官最爱追问的封装思路。不管是零基础想入门还是已经写了一阵子脚本想进阶都能在里面找到能直接抄作业的内容。2. Web自动化里那些高频函数这样用才对2.1 元素定位与交互不只是find_elementSelenium里最基础的是find_element和find_elements但实际项目里很少有人直接裸调这两个方法。原因很简单裸调时元素没出现就直接抛异常没有任何等待和重试而且定位失败的报错信息很干瘪你不知道是哪个页面、哪个元素出了问题。我习惯在项目里封装一个基础操作层核心思路是“点击前必须可见可点输入前必须清空再输入每次操作都记录日志”。你可以直接参考这段思路from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import allure class BasePage: def __init__(self, driver, timeout10): self.driver driver self.timeout timeout def find(self, by, locator, timeoutNone): timeout timeout or self.timeout try: el WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located((by, locator)) ) except Exception as e: # 出现异常时截图并附加到测试报告方便排查 allure.attach(self.driver.get_screenshot_as_png(), namefind_fail_shot, attachment_typeallure.attachment_type.PNG) raise TimeoutError(f元素定位超时: {by}{locator}, 等待 {timeout}s) from e return el def click(self, by, locator, timeoutNone): el self.find(by, locator, timeout) el.click()这里有两个容易被忽略的细节。第一个细节是等待条件的选择。expected_conditions里的presence_of_element_located只关心元素在DOM里存在哪怕它被遮挡、不可见、不可点击照样返回。所以对于点击操作更稳妥的等待条件是element_to_be_clickable它额外校验了可见性和可用性。不少新手明明等了很久还是报错就是因为等了“存在”但按钮始终是disabled状态。第二个细节是超时时间要不要暴露成参数。我见过有些人把等待时间写死在方法内部等到某个页面加载特别慢时只能临时改源码。正确做法是给每个交互方法都留一个timeout参数默认用类里面的全局值特殊场景再单独传。这样既保持统一性又保留灵活性。再顺带说一下Playwright。它的定位语法比Selenium更简洁比如page.locator(text登录)、page.get_by_role(button, name登录)而且locator对象自带wait_for和自动等待机制。用Playwright写自动化时很多Selenium里需要手写WebDriverWait的场景都不需要了因为它在执行click、fill之前会自动等待元素可操作。但要注意Playwright的自动等待不等于完全不需要等待函数比如某个接口返回特定数据后才出现的元素它等不了业务条件还是得用expect(locator).to_be_visible()这类显式断言等待。2.2 等待函数隐式、显式、强制等待的一次说清Web自动化的等待问题永远是面试和实战的高频区。我把三种等待按照“力度从弱到强”排个序隐式等待、显式等待、强制等待。隐式等待是给driver设置一个全局轮询时间session级别生效。比如driver.implicitly_wait(10)意味着每次find_element时如果元素没立刻出现会在10秒内不停轮询直到出现或超时。它的问题有两个一是只在find_element时生效对元素不可见、不可点击的情况无感二是如果和显式等待混用等待时间会叠加比如隐式10秒加显式10秒最坏情况下要等20秒才抛异常极大拖慢用例执行。显式等待是最推荐的方式它针对某个具体条件做轮询精度高、定位准。我常用的场景有from selenium.webdriver.common.by import By 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.element_to_be_clickable((By.ID, submit-btn))) # 等待某个元素文本变成指定内容适合校验操作结果 wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, .toast), 保存成功))poll_frequency这个参数很多人不设默认0.5秒轮询一次。实测中如果页面响应很快可以把poll_frequency调低到0.1秒用例能快一点反之页面很卡就调到1秒减少无谓的轮询请求。强制等待也就是time.sleep我把它归为“最后的手段”。它的问题不是不能用而是用起来太“无脑”不管页面实际好了没有统一睡满时间。页面快了浪费时间页面慢了照样失败。所以它只适合调试阶段、模拟人工思考时间、或者处理确实无法被常规等待覆盖的外挂行为。我在代码审查时看到time.sleep(5)这种写法第一反应都是问这里为什么不用显式等待如果确实要保留我会建议加个注释说明等待的业务原因是哪个。3. 接口自动化中的常用函数从requests到pytest的默契配合3.1 请求函数与响应处理Web自动化解决的是UI层面接口自动化则是绕过界面直接验证服务端逻辑。接口自动化里最常用的第三方库就是requests它足够轻量生态也稳。不过裸用requests也有问题每个用例都要拼URL、带headers、处理cookie、判断状态码代码重复率极高。所以接口自动化项目里我第一件事就是封装一个统一请求入口。import requests from requests import Session class ApiClient: def __init__(self, base_url): self.base_url base_url self.session Session() def request(self, method, path, **kwargs): url self.base_url path # 统一设置超时与证书校验避免用例里反复写 kwargs.setdefault(timeout, 10) kwargs.setdefault(verify, False) resp self.session.request(method, url, **kwargs) # 记录日志方便排查失败用例 print(f[API] {method.upper()} {url} - {resp.status_code} {resp.elapsed.total_seconds():.2f}s) return resp这里的Session很重要。requests的Session对象会自动管理cookie保持跨请求的登录状态比每次单独用requests.get/post更接近真实浏览器的会话行为。另一个容易被忽略的是resp.elapsed它返回请求总耗时的timedelta对象可以换算成毫秒用来做性能断言。响应处理我建议再补一层。大多数接口返回JSON直接resp.json()是可以但当你需要同时断言状态码、响应时间和响应体时代码会越来越碎。我会封装一个统一的断言入口class ApiAssert: staticmethod def assert_response(resp, expect_status200, expect_keyNone, expect_valueNone): assert resp.status_code expect_status, \ f状态码不符期望{expect_status}实际{resp.status_code}响应体{resp.text} if expect_key: data resp.json() assert expect_key in data, f响应中缺少字段: {expect_key} if expect_value is not None: assert data[expect_key] expect_value, \ f{expect_key}值不符期望{expect_value}实际{data[expect_key]}封装完以后测试用例的阅读成本会明显下降。比如“创建用户后校验返回的id大于0”写出来的用例几乎是自然语言def test_create_user(api_client): resp api_client.request(post, /api/user, json{name: 张三}) ApiAssert.assert_response(resp, expect_status200) user_id resp.json()[id] assert user_id 03.2 让测试数据活起来随机数据与格式化函数接口测试数据如果全靠手写有几个痛点姓名重复导致唯一性校验失败、手机号不符合规则、时间戳永远是同一时刻。这时候用程式化生成数据是最好的选择。faker这个库在测试圈已经算标配了。它支持中文环境能生成姓名、手机号、身份证、地址等常用测试数据用法非常直接from faker import Faker fake Faker(localezh_CN) # 生成随机姓名与手机号 name fake.name() phone fake.phone_number() id_card fake.ssn()注意一点faker生成的手机号并不保证符合运营商号码段规则有些系统会在后端校验手机号合法性这时就需要自己实现一个“假手机号生成函数”按“1[3-9]开头9位随机数”的规则去构造并且要避开已存在于测试库中的号码。再就是时间处理。日期时间相关函数在任何自动化项目里都躲不掉我的通用做法是统一用datetime和time模块封装几个工具函数from datetime import datetime, timedelta def get_now_str(fmt%Y-%m-%d %H:%M:%S): return datetime.now().strftime(fmt) def get_future_date(days30): return (datetime.now() timedelta(daysdays)).strftime(%Y-%m-%d)为什么单独封装因为直接裸写datetime.now().strftime(...)本身不难但分散在几十个用例里一旦要统一改时间格式比如从年月日变成时间戳逐个修改非常痛苦。集中封装后改一个函数里的fmt所有用例都生效。JSON序列化也是重灾区。很多接口返回的数据里带有中文requests的resp.json()本身没问题但如果你要把响应体写入日志或文件用json.dumps(data, ensure_asciiFalse)才能保留中文。不写ensure_asciiFalse的话中文会被转成\uXXXX日志内容基本没法直接看。这个小细节几乎每个新人都中招过。4. 数据驱动与测试固件pytest里不可忽略的函数式组装4.1 fixture、conftest与参数化的组合拳pytest是当前接口自动化领域事实上的标准框架它的fixture机制能用函数组装出非常优雅的依赖注入能力。fixture的本质就是一个带yield的普通函数yield之前的代码是setupyield之后的代码是teardownyield本身的返回值就是注入给测试用例的参数。比如登录token这个东西多个接口都要用。与其在每个用例里都走一遍登录接口不如定义一个login_token的fixtureimport pytest from api_client import ApiClient pytest.fixture(scopesession) def api_client(): client ApiClient(https://api.example.com) return client pytest.fixture(scopesession) def token(api_client): resp api_client.request(post, /api/login, json{username: admin, password: 123456}) data resp.json() assert data[code] 0, f登录失败: {data} return data[token]scopesession保证整个测试会话只登录一次极大减少重复请求。如果登录状态在session期间会过期那就改用scopemodule或者scopeclass让每个测试模块/类自己刷新登录态这是工程化里一个很真实的权衡。参数化的作用则是把同一个测试逻辑喂给多组数据。最朴素的用法import pytest pytest.mark.parametrize(a,b,expected, [ (1, 2, 3), (10, 20, 30), (-1, 1, 0), ]) def test_add(a, b, expected): assert a b expected真正做接口自动化时参数化数据通常不直接写在代码里而是从外部文件或数据库读取。上面那个parametrize的列表完全可以由一个读取数据文件的函数动态生成这样测试用例和测试数据就彻底解耦了新增一个用例不需要改代码。4.2 从文件读数据的三板斧Excel、JSON、YAML日常接口测试中测试数据最常见的三种载体是Excel、JSON、YAML。Excel适合给业务同事维护JSON适合配置信息YAML适合管理用例结构。三个库分别对应openpyxl、json、PyYAML的yaml.safe_load。Excel读取我踩过不少坑这里直接给你一个可用的封装from openpyxl import load_workbook def read_excel(file_path, sheet_nameNone): wb load_workbook(file_path, data_onlyTrue) ws wb[sheet_name] if sheet_name else wb.active rows ws.iter_rows(values_onlyTrue) headers next(rows) data [] for row in rows: # 跳过空行 if all(cell in (None, ) for cell in row): continue data.append(dict(zip(headers, row))) return datadata_onlyTrue这个参数是重点。Excel里很多单元格是公式如果没开data_onlyopenpyxl读到的就是公式字符串而不是计算结果断言时必然对不上。另外一个坑是如果文件是用WPS或Excel编辑后没保存过自带的缓存结果可能是空的你会拿到一堆None值。遇到这种情况记得先检查文件本身是否有计算结果。JSON和YAML读取更简单但要注意yaml.safe_load和yaml.load的区别yaml.load不加Loader参数在PyYAML 5.x之后会直接抛警告并最终报错所有解析YAML的地方都应该无脑用safe_load它只解析标准数据结构防止任意代码执行。这个知识点在软件测试面试题里也经常被问到。文件名处理建议用pathlib库比如Path(file).parent.parent / data / user_cases.yaml这样不管在本地还是CI环境执行路径都能正确解析避免使用相对路径时“当前目录不对所以找不到文件”的经典问题。5. 排查实录常用函数用不好bug藏在细节里5.1 高频报错与应对速查表自动化测试跑起来之后真正的学习才刚刚开始。我整理了一张高频报错速查表都是团队项目里真实出现过的异常类型出现的典型场景排查方向NoSuchElementException元素定位失败先确认是否在iframe内、是否新窗口再确认定位表达式是否被异步数据影响StaleElementReferenceException元素先找到后DOM刷新导致引用失效避免在循环里持有同一个元素引用每次操作重新查找是列表页的高发问题TimeoutException显式等待超时检查等待条件是否写错、元素是否在另一个frame、接口是否真的返回了ConnectionError接口请求连接失败检查网络、代理配置、服务是否启动利用Session重试机制实现二次请求UnicodeEncodeError日志或文件写入中文乱码统一指定encodingutf-8JSON字符串用ensure_asciiFalse有一个很多人忽略的要点NoSuchElementException出现时第一件事不是刷新页面或改代码而是打开观察页面当前状态。很多自动化脚本之所以失败是因为上一个用例把页面状态污染了比如弹窗没有关闭、列表页翻到了第3页。这时候如果盲目修改定位表达式只会越改越崩。5.2 我实际踩过的三个典型的坑第一个坑是显式等待“形同虚设”。我之前封装点击方法时等待条件用了presence_of_element_located觉得元素只要存在就够了。结果遇到一个场景某个按钮确实在DOM里但被一个全屏遮罩挡住等它可点等了半天还是超时。排查到最后发现问题是等待条件不对应该用element_to_be_clickable换成之后问题立刻消失。这个经历让我养成了一个习惯点击类操作统一用可点击条件输入类操作用可见条件存在类条件只用于校验页面是否出现了某个节点。第二个坑是openpyxl读到一堆None。当时测试数据管理从手工造数切到Excel读取结果跑出来的用例全是断言失败。排查后发现是同事直接把公式写在了Excel里而data_only默认是False读到的是公式本身自然和期望值对不上。从那次以后我的Excel读取封装里强制设置data_onlyTrue并且在上线前校验读取结果里不能出现空值。第三个坑是pytest参数化传错了对象。有一次我写参数化数据源把“随机生成手机号”的函数对象直接放进用例列表而不是函数调用的返回值。pytest确实能跑读到的不是手机号而是一个function内存地址。这类问题日志里极其难看出来因为它不报错只是数据永远是同一串地址。排查方法是在参数化数据周边打日志把每个用例的入参实际值打印出来看一眼。6. 面试常见考点怎么回答“你封装过哪些常用函数”6.1 从面试官视角看函数封装软件测试面试题里面有一个出现频率极高的追问你们项目里的公共函数是怎么封装的这个问题表面在问函数实际在考察你的抽象能力、异常处理意识和工程化思维。面试官想听的不是“我会用requests.get”这种废话而是你能不能说清楚哪些逻辑被抽出来为什么抽抽完之后解决了什么问题。我建议的回答结构是“点线面”。先说点我项目里常用的一等公民函数包括元素点击封装、等待封装、接口请求封装、数据生成封装、断言封装、日志封装。再说线点击和输入会先走等待等待失败会截图并记录日志接口请求统一带超时和Session断言统一走断言类。最后说面这些封装组合起来形成了一套公共工具层业务用例只关心Given-When-Then不直接碰底层驱动或requests细节。这里有个加分项是异常处理细节。如果面试官问“你最喜欢自己封装的哪个函数”你可以说“安全执行函数”。它的思路是接收一个函数和它的参数执行时捕获异常、记录上下文、返回失败标识让主流程不会被一个元素的偶发异常直接打断import functools import traceback def safe_call(func, *args, retry2, **kwargs): for attempt in range(retry 1): try: return func(*args, **kwargs) except Exception: if attempt retry: traceback.print_exc() return None return None这个函数体现的工程思想很值钱自动化测试真正稳定运行靠的不是“每条用例都一次过”而是“失败时不至于整个测试进程崩溃同时留下足够信息定位问题”。6.2 把常用函数组织成自己的测试工具库函数封装到最后不是零散的一堆方法而应该沉淀成一套工具库。我通常按这样的目录组织common/ ├── base_page.py # Web自动化页面基类 ├── api_client.py # 接口请求封装 ├── data_factory.py # 测试数据生成 ├── file_reader.py # Excel/JSON/YAML读取 ├── assertion.py # 公共断言 └── log_util.py # 日志与报告这套设计的关键判断标准是一个新入职的测试工程师拿到代码不需要问太多问题就能在一个用例文件里读懂“在测什么”。如果某个封装让代码看起来更像天书或者为了一个只在两台机器上出现的问题写了好几层装饰器那就是过度设计了。命名规范也是面试和实际审查的注意点。动作对象的命名方式最稳比如click_button、fill_input、get_cell_value一看名字就知道在干嘛。反面教材就是写一堆do_stuff、handle_data三个月后自己也看不懂。维护工具库时我有一条红线尽量不用超过三层的函数调用链。太深了出问题时顺着调用栈追下去血压真的会升高。说到底自动化的常用函数不是背出来的知识点而是一边踩坑一边提炼出来的“手艺”。我每次把一段重复代码抽成函数时都会顺手补上日志和失败现场截图这个习惯帮我在无数个深夜定位问题时少走了很多弯路也真心建议正在看这篇文章的你从下一个用例开始就这么干。