
测试工作干了几年之后我越来越觉得自动化测试这件事儿难的不是代码本身而是很多人一开始就把路走偏了。有人花了几周啃框架源码到头来连一条稳定的用例都跑不过夜有人一上来就买了各种付费工具结果发现一个内部管理系统根本用不上那些重量级方案。我写这篇文章的目的很简单——把我这些年用 Python 做自动化测试的完整套路、能直接抄的代码和那些文档里不会写的坑一次说清楚。标题是如何用 Python 实现自动化测试但我不会只丢给你几个示例而是从选型、环境搭建、框架设计到报告产出把整条链路走一遍。覆盖 UI 自动化和接口自动化两条主线适合刚准备入门自动化测试的测试工程师也适合后端或前端开发想补测试技能的读者。1. 先搞清楚自动化测试到底在解决什么问题很多新手问我第一个问题往往是哪种自动化工具最好用但以我的经验来看这不是最先该问的问题。最先该问的是你的项目到底适不适合做自动化以及你希望自动化帮你省下哪种成本。方向没想清楚后面每一步都会别扭。1.1 手动回归的痛点为什么功能测试越做越累一个常规的 Web 项目走到稳定期之后最消耗人力的往往不是新功能测试而是回归测试。新功能可能一周只改两三个模块但回归要把核心流程全部跑一遍——登录、下单、支付、退款、消息通知全手工点一遍至少两小时而且点了三个月之后你会发现自己根本提不起兴致点错按钮的概率反而比刚接手时更高。自动化测试解决的核心就是这类重复劳动。把固定的操作步骤固化成脚本把期望结果固化成断言交给机器去跑。人从手动点击者变成结果审核者这种转变才是自动化真正的价值所在。不是说自动化能发现特别多新 Bug它的主要战场是防止老功能被改坏也就是回归保护。1.2 什么项目适合上自动化什么项目别硬上我见过最惨的案例是有人拿自动化去跑一个还在频繁改版的活动页。昨天按钮 id 还叫submit_btn今天产品经理说改成预约前端把 id 改成了reserve_btn脚本只能跟着改改到第三个版本的时候已经没人愿意维护了。自动化测试不是万能的它最适合的是以下类型核心业务流程相对稳定的系统比如后台管理系统、交易系统、订单系统需要频繁回归的版本迭代比如每周发版的敏捷项目数据量大、需要大量参数组合验证的场景比如接口测试中成百上千条用例需要在多种浏览器或多套环境上重复执行的跨端验证反过来如果你的项目还在三天两头推翻重做的阶段或者 UI 样式极其复杂并且没有任何可用的定位标识我建议先把功能测试流程理顺再考虑自动化不要为了自动化而自动化。1.3 自动化测试的三个层级从底到顶怎么分工做自动化之前先了解层级划分因为不同层级维护成本和收益差别非常大。单元测试测试函数、类、接口内部逻辑由开发同学写跑得最快定位最准确接口测试直接对服务的 HTTP 接口、RPC 接口发请求验证不依赖 UI稳定性高是 ROI 最高的层级UI 测试模拟真实用户操作界面最接近用户行为但也最脆弱页面稍微改个样式脚本可能就挂了以我的实践来看一般的 Web 项目里接口测试至少应该占 60% 以上的自动化用例量UI 测试控制在 20%-30% 比较合理剩下的留给单元测试。但很多团队自动化刚起步时都从 UI 做起因为老板看得到效果——一个脚本自动打开浏览器在那儿点点点演示就很有冲击力。实际用下来你也会发现 UI 测试确实最容易出成就感所以我后面先讲接口自动化再讲 UI 自动化但按投入产出比接口自动化其实更值得先做扎实。2. 动手前的技术选型为什么主流方案是 pytest Requests Selenium选型这件事听别人推荐永远都是片面的但如果你想少走弯路我建议你直接参考当前社区里接受度最高、文档最全、出问题最容易搜到答案的组合。不要搞花活不要搞冷门框架否则你的大部分时间会花在给框架填坑而不是写业务用例上。2.1 Python 为什么成为自动化测试的主流语言Python 能成为自动化测试的主流语言不是因为它性能多好而是因为它的开发效率和生态实在太适合测试场景了。语法简单测试代码本身就是一种可读需求文档非开发出身的测试人员上手门槛低第三方库极其丰富Web 自动化有 Selenium、Playwright接口测试有 Requests移动端有 Appium基本你要什么都有现成的和 CI/CD 工具链集成方便Jenkins、GitLab CI 都能直接拉 Python 脚本执行pytest 这个测试框架的插件体系非常强大断言、固件、参数化、报告生成都做得非常成熟对于一个测试团队来说Python 意味着即使你没有专职的测试开发普通测试工程师经过两到三周的系统学习也能写出能用的自动化脚本。这比 Java 那套的 Spring 全家桶轻量太多。2.2 pytest 和 unittest、Robot Framework 的取舍Python 里做自动化测试绕不开测试框架的选择。我先给结论现在做新项目直接选 pytest 就好除非公司已有既定技术栈。如果是 unittest它是 Python 标准库自带的不用装额外依赖但写起来比较啰嗦类继承那套风格对新手不友好断言方法也得记一大堆assertEqual、assertTrue不像 pytest 直接用assert一个关键字就搞定。而且 pytest 能原生运行 unittest 风格的用例所以从 unittest 迁到 pytest 成本很低。Robot Framework 最大特点是关键字驱动特别是它的 RIDE 编辑器可以拖拖拽拽写用例适合完全不懂编程的测试同学。但用了一段时间你会发现一旦用例数量涨到几百条关键字封装就成了新的维护负担想写复杂逻辑时 Robot Framework 反而变成束缚。我的建议是团队里全是会写代码的人就直接 pytest有纯手工测试且完全不会编程的成员Robot Framework 可以作为一个过渡方案但长期来看还是得把代码能力补起来。2.3 接口测试为什么选 RequestsUI 测试为什么选 SeleniumRequests 是 Python 里做 HTTP 请求的事实标准。它封装了 urllib 那套复杂的操作一行代码就能发一个带 Headers、Cookies、参数的请求对接口测试来说这是最核心的能力。配合 pytest 的参数化几百个接口用例可以全用数据驱动的方式组织起来。Selenium 则是 Web UI 自动化里最经典的方案。它通过 WebDriver 协议操作浏览器支持 Chrome、Firefox、Edge、Safari 等主流浏览器。虽然现在 Playwright 在某些方面更现代比如自动等待机制、多标签页处理都比 Selenium 好但 Selenium 的社区积累实在太大你遇到的 90% 问题都能在 Stack Overflow 上查到答案新手用它起步最稳妥。等 Selenium 玩熟了再去看 Playwright 也完全不亏。注意Selenium 4 之后官方建议直接使用 WebDriver Manager 或者 Selenium Manager 来管理浏览器驱动不用再手动下载 chromedriver 放到环境变量里。这个变化大大降低了环境配置的门槛。2.4 自动化测试框架的完整能力图一个能被正式项目使用的自动化测试框架至少要具备下面这些能力这也是面试时经常被问到的自动化测试框架应该包含哪些模块用例管理命名规则、执行顺序、跳过条件、失败重跑数据驱动从 Excel、JSON、YAML 读取测试数据拉通用例和数据的分离断言机制支持丰富的断言写法失败时能清晰展示期望值和实际值差异报告输出向 Allure、HTML 报告插件输出可读性强的测试结果日志系统记录请求参数、返回结果、操作步骤信息方便定位问题CI 集成支持命令行一键执行、生成 JUnit XML 报告、设置退出码现在很多网上的自动化测试框架源码看着很复杂动不动就几万行代码其实核心就是把这些能力组装起来。你自己用 pytest 搭一个轻量级框架十五到二十个文件就够了。3. 环境搭建从 Python 解释器到项目骨架准备好开始动手了吗环境搭建这一节我不会只丢几条命令因为这一步恰恰是新手最容易出问题的环节。你会遇到版本不兼容、pip 安装失败、Chrome 打不开等等一堆奇奇怪怪的问题我把常见情况都给你列出来。3.1 安装 Python 解释器版本选择和安装过程Python 的版本选择上我的建议是装 3.9 或更高版本避开社区还不兼容的最新大版本。比如现在 Python 3.13 发布了但如果你的第三方库还没适配就可能会踩坑。3.10 或者 3.11 是当前兼容性最好的选择。Windows 安装很简单去官网下载安装包安装的时候一定勾选Add Python to PATH然后选Customize installation。这个 PATH 选项如果没勾选后面在命令行里输入python就会提示找不到命令很多新手卡在这一步。macOS 上如果熟悉 Homebrew直接brew install python3.11就行。装完后在终端里验证一下python --version pip --version如果pip提示找不到可以试一下python -m pip --version。这两个命令能正常输出就说明 Python 环境没问题了。3.2 虚拟环境为什么你必须用 venv很多自动化测试项目跑到一半出问题就是环境依赖互相污染导致的。你可能今天在电脑上装了 A 库明天给另一个项目装了 B 库两个项目需要同一个库的不同版本结果互相覆盖项目就崩了。虚拟环境就是解决这个问题的。Python 自带venv模块不需要额外安装。在项目目录下执行python -m venv venvWindows 激活虚拟环境venv\Scripts\activatemacOS 和 Linux 激活source venv/bin/activate激活后你会在命令行前面看到(venv)标识此后所有pip install都会安装到这个虚拟环境里跟系统环境完全隔离。这个习惯一定要从第一天就养成否则后面越做越乱。3.3 安装核心依赖库创建一个requirements.txt文件写入以下核心依赖pytest8.2.0 requests2.32.3 selenium4.21.0 webdriver-manager4.0.1 pytest-html4.1.0 allure-pytest2.13.5然后执行安装pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple镜像地址这里我给了清华源国内网络环境下安装速度会快非常多。如果你是公司内网环境可能还要配置专门的企业 PyPI 源具体可以问运维同事。3.4 项目目录结构规划自动化测试项目和功能代码一样结构好不好直接决定后续维护成本。我常用的目录规划如下auto_test_project/ ├── config/ # 配置文件环境地址、账号信息等 │ └── config.yaml ├── data/ # 测试数据文件 │ └── test_cases.xlsx ├── test_cases/ # 测试用例目录 │ ├── test_login.py │ └── test_order.py ├── pages/ # UI 自动化页面对象PO模式 │ ├── login_page.py │ └── order_page.py ├── common/ # 公共方法封装 │ ├── requests_util.py │ └── logger_util.py ├── reports/ # 测试报告输出目录 ├── logs/ # 日志文件目录 ├── conftest.py # pytest 固件定义 ├── pytest.ini # pytest 配置 └── requirements.txt这个结构不是我拍脑袋定的它是一个典型的数据、用例、对象、工具分层架构。新手可能觉得多但当你写的用例超过五十条的时候你会发现分层的好处是建立在无形的规则里的——测试数据变了不用改代码页面对象变了不用改用例环境变了不用动脚本。4. 接口自动化测试实战基于 Requests pytest 写一套可复用的登录测试接口自动化作为 ROI 最高的层级我建议每个人都从它先入手。这一节我们不讲空泛的概念直接用一个登录接口的例子把 Requests 发请求、pytest 组织用例、断言校验、参数化跑多条数据的全流程走通。4.1 被测接口分析登录接口的请求格式与校验点假设被测系统有一个登录接口POST http://test-api.example.com/api/login Content-Type: application/json { username: test_user, password: 123456 }正常响应{ code: 0, message: success, data: { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 } }我们测试这个接口要验证的点包括状态码是否为 200返回的 JSON 中code是否为 0返回的message是否为success登录成功后data.token是否存在密码错误时是否返回对应的错误码和提示信息这些校验点其实就是接口测试的断言来源。写测试用例之前先把预期想清楚比上来就敲代码更重要。4.2 封装请求工具类让每条用例都少写三行代码如果不做任何封装一个接口用例大概要写十行左右但如果把所有接口用例都切换到另一个环境你会发现每个用例里的 URL 都要改这是灾难。所以我先封装一个common/requests_util.py把请求基础地址、超时时间、公共请求头统一处理。# common/requests_util.py import requests import json class RequestsUtil: def __init__(self, base_url): self.base_url base_url self.session requests.Session() self.headers {Content-Type: application/json} def post(self, path, **kwargs): url self.base_url path if headers in kwargs: kwargs[headers].update(self.headers) else: kwargs[headers] self.headers if timeout not in kwargs: kwargs[timeout] 10 return self.session.post(url, **kwargs) def get(self, path, **kwargs): url self.base_url path if headers in kwargs: kwargs[headers].update(self.headers) else: kwargs[headers] self.headers if timeout not in kwargs: kwargs[timeout] 10 return self.session.get(url, **kwargs)用requests.Session()的好处是可以在多个请求之间自动携带 Cookie比如登录接口返回的 Cookie、Token后续请求就能直接共享这很接近真实用户浏览器的行为。4.3 用 pytest 写第一条接口测试用例在test_cases/下创建test_login.py# test_cases/test_login.py import pytest from common.requests_util import RequestsUtil BASE_URL http://test-api.example.com def test_login_success(): req RequestsUtil(BASE_URL) payload { username: test_user, password: 123456 } response req.post(/api/login, jsonpayload) assert response.status_code 200, fstatus code should be 200, got {response.status_code} body response.json() assert body[code] 0, fbusiness code should be 0, got {body[code]} assert body[message] success assert body[data][token] is not None and body[data][token] ! 执行测试pytest test_cases/test_login.py -v你会看到类似这样的输出test_cases/test_login.py::test_login_success PASSED第一条接口测试用例就跑通了。细心的人会发现断言里面我都写了自定义提示信息这看起来不起眼但当你用例失败时它能告诉你实际拿到的是什么而不是只给你一个光秃秃的AssertionError排查问题的效率完全不一样。4.4 参数化驱动一个函数跑遍全部测试数据登录接口的测试点不止成功一条用例还需要测试密码错误、用户不存在、用户名为空、密码为空等多种情况。如果每个场景写一个函数代码会非常冗余。pytest 的参数化装饰器就是解决这个问题的。# test_cases/test_login.py import pytest from common.requests_util import RequestsUtil BASE_URL http://test-api.example.com # 使用元组列表定义测试数据 TEST_DATA [ (test_user, 123456, 0, success), # 正常登录 (test_user, wrong_pwd, 1001, 密码错误), # 密码错误 (no_such_user, 123456, 1002, 用户不存在), # 用户不存在 (, 123456, 1003, 用户名不能为空), # 用户名为空 (test_user, , 1004, 密码不能为空), # 密码为空 ] pytest.mark.parametrize(username,password,expected_code,expected_msg, TEST_DATA) def test_login(username, password, expected_code, expected_msg): req RequestsUtil(BASE_URL) payload {username: username, password: password} response req.post(/api/login, jsonpayload) assert response.status_code 200 body response.json() assert body[code] expected_code assert body[message] expected_msg运行一下pytest test_cases/test_login.py -v可以看到 5 条用例全部被执行。这就是数据驱动的核心思想测试代码写一套测试数据一条条往里喂。以后新增一条用例只需要在TEST_DATA列表里加一个元组不需要新增函数。如果数据量巨大还能从 Excel、YAML、JSON 文件里读取实现测试数据和代码彻底分离。4.5 接口依赖处理先登录获取 Token再请求需要鉴权的接口真实项目里很多接口都需要鉴权比如查询订单列表之前必须先拿到登录 Token。处理这类依赖有两种常见做法一种是在 fixture 里提前登录并返回 token另一种是直接在一个用例里顺序执行。pytest 的 fixture 机制是更优雅的方案。在conftest.py中定义一个login_tokenfixture# conftest.py import pytest from common.requests_util import RequestsUtil pytest.fixture(scopesession) def login_token(): req RequestsUtil(http://test-api.example.com) payload {username: test_user, password: 123456} response req.post(/api/login, jsonpayload) body response.json() assert body[code] 0 token body[data][token] yield token然后在需要鉴权的用例中直接把login_token作为参数传入# test_cases/test_order.py import pytest from common.requests_util import RequestsUtil BASE_URL http://test-api.example.com def test_get_order_list(login_token): req RequestsUtil(BASE_URL) headers {Authorization: fBearer {login_token}} response req.get(/api/orders, headersheaders) assert response.status_code 200 body response.json() assert body[code] 0 assert isinstance(body[data], list)scopesession表示整个测试会话只登录一次token 全局共享后面所有需要鉴权的用例都复用不会每跑一条用例就登录一次效率高很多。提示login_token这种 fixture 如果被多个测试文件共用一定要把它放在项目根目录的conftest.py里pytest 会自动识别该文件中的 fixture不需要 import。5. UI 自动化实战用 Selenium 模拟真实用户操作接口测试能验证服务和数据逻辑但改变不了前端页面、交互流程的问题。比如一个按钮的点击事件没绑定接口层面你是测不出来的这类问题必须靠 UI 自动化。不过我必须提前给你打预防针UI 自动化是所有层级的测试里最娇气的元素定位和等待策略是两大难点这一节我会重点讲。5.1 浏览器驱动管理告别手动下载 chromedriver 的烦恼以前做 Selenium 测试最烦的一步就是下载和浏览器版本匹配的 chromedriver。Chrome 一升级驱动就对不上了脚本全挂。现在用webdriver-manager这个库可以自动管理驱动版本。# common/webdriver_util.py from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager def create_driver(): service Service(ChromeDriverManager().install()) options webdriver.ChromeOptions() # 无头模式跑测试时不需要弹浏览器窗口 # options.add_argument(--headless) options.add_argument(--start-maximized) options.add_argument(--disable-gpu) driver webdriver.Chrome(serviceservice, optionsoptions) driver.implicitly_wait(10) return driverwebdriver-manager第一次运行时会自动下载匹配的 chromedriver 到本地缓存目录之后每次都复用。如果要跑无头模式把注释掉的--headless参数放开就行。本地调试建议不要用无头模式因为你需要看到浏览器真实的运行过程出了问题一眼就能看出是页面没加载出来还是点击没生效。5.2 八大元素定位方法用最稳的方式找到元素Selenium 定位元素的方式有八种id、name、class name、tag name、link text、partial link text、xpath、css selector。实际项目里我建议的优先级是id css selector xpath 其他。原因是 id 在页面中理论上唯一定位速度最快。但现实是前端工程师在写代码时不一定会给每个元素配 id更多时候需要用 CSS 选择器或 XPath 来兜底。看一个具体的登录页面form idloginForm input idusername placeholder请输入用户名 / input idpassword typepassword placeholder请输入密码 / button idloginBtn typesubmit登 录/button /form对应的 Selenium 定位代码from selenium.webdriver.common.by import By driver.find_element(By.ID, username).send_keys(test_user) driver.find_element(By.ID, password).send_keys(123456) driver.find_element(By.ID, loginBtn).click()如果元素没有 idCSS 选择器是更好的选择driver.find_element(By.CSS_SELECTOR, input[placeholder请输入用户名]).send_keys(test_user)XPath 虽然万能但写复杂了很脆弱。比如绝对路径//html/body/div[1]/div[2]/form/input[1]只要页面层级动一下就废了。要用 XPath 也尽量用相对路径和属性结合的方式比如//input[nameusername]。5.3 显式等待UI 自动化的灵魂新手最容易犯的错误是元素还没加载完就开始点击然后报NoSuchElementException。解决办法不是加time.sleep(3)而是使用等待机制。Selenium 里等待分三种隐式等待设置一个全局超时时间find_element 在找不到元素时会轮询等待显式等待针对特定元素等到某个条件满足再去操作强制等待time.sleep()不推荐因为等待时间固定机器快了浪费时间慢了照样失败我在create_driver里设置了implicitly_wait(10)这算是一道兜底防线。但更精细的场景比如点击登录后等待首页加载用显式等待最稳妥from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 点击登录 driver.find_element(By.ID, loginBtn).click() # 显式等待直到首页的标题文字出现 WebDriverWait(driver, 10).until( EC.text_to_be_present_in_element( (By.ID, welcomeTitle), 欢迎回来 ) )这行代码表达的是最多等 10 秒每 0.5 秒检查一次直到页面出现欢迎回来这四个字。相比固定 sleep这种写法既稳定又高效。5.4 完整 UI 用例登录-首页检查-退出把前面几个点整合起来一条完整的 UI 正常流程用例是这样的# test_cases/test_ui_login.py import time from common.webdriver_util import create_driver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def test_login_and_check_home(): driver create_driver() try: driver.get(http://test-web.example.com/login) driver.find_element(By.ID, username).send_keys(test_user) driver.find_element(By.ID, password).send_keys(123456) driver.find_element(By.ID, loginBtn).click() # 等待页面跳转并断言 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, dashboard)) ) assert driver.title 用户后台首页 # 退出登录 driver.find_element(By.CSS_SELECTOR, .user-avatar).click() driver.find_element(By.LINK_TEXT, 退出登录).click() # 断言回到登录页 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, loginForm)) ) assert driver.current_url.endswith(/login) finally: driver.quit()看到try...finally没这是 UI 自动化里非常重要的一层保险无论用例通过还是失败浏览器最后都会被driver.quit()关闭。如果你不写这一句脚本异常时浏览器会一直留在桌面上几次跑下来内存就被占满了而且很挡视线。5.5 元素定位失败和偶发超时的排查思路跑 UI 自动化一段时间你会遇到两个高频问题元素定位报错和偶发超时。别慌按下面的思路排查大部分问题都能快速定位元素定位失败时把浏览器窗口放大肉眼打开页面确认元素是否真的存在查看页面源码F12 开发者工具确认 id、name、class 是否写错检查页面是否在 iframe 或 shadow DOM 中Selenium 默认只能处理顶层文档需要先切换进去确认元素是否被遮挡比如悬浮层盖住了按钮click 可能会报ElementClickInterceptedException偶发超时时检查是否网络波动造成的静态资源加载慢把隐式等待时间从 10 秒调到 15 秒或者 20 秒优先用显式等待替代隐式等待特别是在登录跳转、数据加载这类耗时不确定的场景6. 让测试代码能一直跑下去的架构设计很多人学会了写用例但项目过了三个月就变成一个测试代码维护地狱——页面稍微改版所有用例都要跟着改。这一节我要讲三个能让自动化测试持续运转的架构级技巧PO 模式、pytest 固件体系和 Allure 报告。6.1 PO 模式把页面细节和测试逻辑分开POPage Object模式核心思想是把一个页面封装成一个类类里面放这个页面的元素定位和操作方法测试用例只调用这些方法不直接接触 Selenium 元素定位代码。它的好处是如果页面元素变了只需要改 Page 类里的一个方法所有调用这个方法的用例都不需要动。比如登录页我们可以封装成这样# pages/login_page.py 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: def __init__(self, driver): self.driver driver def open(self, url): self.driver.get(url) def input_username(self, username): element WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.ID, username)) ) element.clear() element.send_keys(username) def input_password(self, password): element WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.ID, password)) ) element.clear() element.send_keys(password) def click_login(self): self.driver.find_element(By.ID, loginBtn).click() def login(self, username, password): self.input_username(username) self.input_password(password) self.click_login()对应的测试用例就变得非常干净# test_cases/test_ui_login.py from common.webdriver_util import create_driver from pages.login_page import LoginPage def test_login_po_mode(): driver create_driver() try: login_page LoginPage(driver) login_page.open(http://test-web.example.com/login) login_page.login(test_user, 123456) assert driver.title 用户后台首页 finally: driver.quit()对比一下你就会发现PO 模式下测试用例读起来就像需求文档打开登录页、输入用户名、输入密码、点击登录、验证标题。它把怎么做的细节全部塞进了 Page 类测试用例只关心做什么。这就是可维护性的真正来源。6.2 pytest 固件体系自动化里的公共底座除了前面提到的login_tokenpytest 的 fixture 还适合做以下事情创建 WebDriver 实例并在用例结束后自动关闭连接测试数据库并清理测试数据从环境变量或配置文件中读取环境配置统计每个用例的执行时间超时的自动标记一个非常实用的组合是把 WebDriver 做成 fixture这样用例之间就不会互相影响浏览器会话# conftest.py import pytest from common.webdriver_util import create_driver pytest.fixture def driver(): driver create_driver() yield driver driver.quit()然后用例直接调用def test_login_ui(driver): driver.get(http://test-web.example.com/login) ...fixture 还有一个很强大的能力是request内置参数可以动态获取当前测试函数的名称、所在模块等信息。比如你想为每个用例都打印一条日志写明现在在跑哪个用例就可以在 fixture 里操作。6.3 测试报告生成用 Allure 让结果看得见没有报告的自动化测试相当于闭着眼睛开车。pytest 运行完默认输出是全文本的老板看不明白开发同学也不爱看。Allure 是目前最流行的测试报告工具它把测试结果渲染成 HTML 网页包含用例分组、步骤、失败截图、参数数据等非常直观。安装依赖我已经放在requirements.txt里了使用需要两步第一步运行测试时指定输出报告数据目录pytest test_cases/ -v --alluredir./reports/allure-results第二步启动一个本地服务预览报告allure serve ./reports/allure-results如果allure命令行工具没有安装要先安装。在用例里还能添加丰富的信息让报告更有价值import allure import pytest allure.feature(登录模块) allure.story(正常登录) allure.title(测试正确用户名密码可以登录成功) def test_login_success(): with allure.step(输入用户名): login_page.input_username(test_user) with allure.step(输入密码): login_page.input_password(123456) with allure.step(点击登录): login_page.click_login() with allure.step(验证标题): assert driver.title 用户后台首页报告里每个 step 都会展开失败的时候你能清楚地看到是哪一步出了问题而不是一坨报错信息堆在那里。6.4 日志记录每条用例都留下完整的操作证据报告告诉你哪条用例挂了日志则告诉你执行到哪一步挂的请求了什么返回了什么。接口测试尤其需要日志因为失败了要能看到请求报文和响应报文方便复现。我常用的日志配置是写到文件 控制台双输出按天滚动# common/logger_util.py import logging import os from datetime import datetime LOG_DIR logs os.makedirs(LOG_DIR, exist_okTrue) log_file os.path.join(LOG_DIR, ftest_{datetime.now().strftime(%Y%m%d)}.log) logger logging.getLogger(auto_test) logger.setLevel(logging.INFO) file_handler logging.FileHandler(log_file, encodingutf-8) file_handler.setFormatter(logging.Formatter(%(asctime)s - %(levelname)s - %(message)s)) console_handler logging.StreamHandler() console_handler.setFormatter(logging.Formatter(%(asctime)s - %(levelname)s - %(message)s)) logger.addHandler(file_handler) logger.addHandler(console_handler)在接口用例中配合使用from common.logger_util import logger def test_login_with_log(): req RequestsUtil(BASE_URL) payload {username: test_user, password: 123456} logger.info(f【登录接口】请求参数: {payload}) response req.post(/api/login, jsonpayload) logger.info(f【登录接口】响应状态码: {response.status_code}) logger.info(f【登录接口】响应内容: {response.text}) assert response.json()[code] 0别小看这几行日志真正定位线上环境、测试环境问题时日志比任何调试器都管用。别人还在一个一个 print 排查的时候你打开日志文件就能看到完整链路。6.5 集成到 CI/CD让自动化测试成为发布流程的一部分自动化测试最大的价值是能在代码变更后自动运行而不是等人手动触发。我在很多项目里配合用的持续集成工具是 Jenkins核心配置就两步第一步在 Jenkins 的构建步骤中添加 Shell 命令或 Windows 批处理命令先进入虚拟环境再执行测试cd $WORKSPACE source venv/bin/activate pip install -r requirements.txt pytest test_cases/ -v --alluredir./reports/allure-results --maxfail5第二步配置 Publish Allure Report 插件让测试报告在构建结束后自动展示在 Jenkins 页面上。这样开发提交代码后流水线会自动跑一遍自动化用例如果挂了相关同事能第一时间看到是哪个模块、哪条用例出了问题。要注意的是CI 环境里跑 UI 测试时通常没有显卡支持需要开启无头模式并且在代码里处理浏览器驱动路径。我自己踩过一次坑CI 上跑 Selenium 一直报cannot find Chrome binary结果是因为 Jenkins 服务运行的用户环境变量里根本没有 Chrome 的安装路径后来在配置里显式指定了chrome_options.binary_location才解决。7. 移动端和其他方向Appium 与更多可能性聊完 Web 自动化和接口自动化你可能还会被问到移动端 App 测试怎么办。热搜词里也出现了 Appium 的身影我就稍微展开一下。7.1 Appium 自动化测试的基本套路Appium 是移动端 UI 自动化的主流工具原理和 Selenium 类似但它中间多了一层 Appium Server通过它去驱动 iOS 的 XCUITest 和 Android 的 UIAutomator2。跑 Appium 至少需要有 Android SDK 和 Appium Server步骤比 Web 自动化繁琐。它的核心代码和 Selenium 很像只是启动参数要指定设备信息和 App 路径from appium import webdriver desired_caps { platformName: Android, platformVersion: 12.0, deviceName: emulator-5554, appPackage: com.example.app, appActivity: .MainActivity, noReset: True } driver webdriver.Remote(http://127.0.0.1:4723/wd/hub, desired_caps)在手机模拟器里启动一个 App 之后后续的点击、输入、断言逻辑和 Selenium 非常像。移动端元素定位方式多了 id、xpath、accessibility id 等但核心思维不变。我的建议是如果你所在团队已经有 Web 自动化基础Appium 的学习曲线不会太陡但移动端测试涉及模拟器启动、真机连接、App 安装包管理、网络代理抓包这些环境层面的问题一定要做好打持久战的准备不要指望一两天全部打通。7.2 从 Selenium 到 Playwright要不要迁移Playwright 是近几年很火的浏览器自动化工具它最明显的改进在三个地方一是自动等待机制更智能基本不需要隐式等待配置二是多浏览器多标签页操作更方便三是可以录制用户操作并自动生成代码对快速搭脚本很有帮助。但是否要迁移我持保守态度。如果你的团队 Selenium 用例已经积累了几百条完全没必要为了更新而迁移。只有当你在新项目里做 UI 自动化时可以评估 Playwright看看它的录制脚本能力和自动等待是否更顺手。自动化测试追求的是稳定和可维护不是工具的新旧。7.3 自动化测试能力模型面试官究竟在看什么热搜词里反复出现自动化测试面试题我也说说招聘面试时最看重的能力。不要背题把下面这几点吃透比刷一百道面试题都有用。能不能说清楚你做过的自动化测试项目整体架构用到了哪些工具和框架能不能解释页面元素定位策略选择的依据而不是只会写 xpath能不能处理接口依赖和测试数据处理比如登录态管理、加密参数处理能不能把测试结果接入 CI让自动化稳定定期执行遇到偶发失败时从哪些维度排查是不是只会把 sleep 时间调大这些能力都来自动手做项目所以如果你现在在准备面试我建议不要只翻面试题而是要跑通一个不少于五十条用例的完整项目遇到问题并解决这些真实经历才是最有说服力的。8. 我踩过的几个坑提前帮你排掉最后这一节从个人经验的角度说说我在自动化测试项目里真正踩过的坑。这些坑很多不是技术难点但一个就够你折腾好几天。8.1 测试数据污染问题接口测试和 UI 测试最怕数据污染。比如测试创建订单的用例执行了两次第二次跑可能因为订单号重复导致失败。解决思路是每条用例都要准备好符合独立性的测试数据具体做法包括用例执行前先造一份特定前缀的数据、执行后在清理阶段删掉、用唯一的随机数做数据标识。我在conftest.py里通常会写一个cleanup_test_data的 fixture在每个用例结束后调用清理接口或数据库操作。8.2 断言写得太弱导致漏测有一种用例叫假通过比如断言只写了assert response.status_code 200但接口内部可能返回了业务错误码。这是新手最常犯的错。接口测试的断言至少要覆盖三层状态码、业务码、关键字段值。UI 测试也类似点击登录按钮后不能只看浏览器没报错就算通过一定要断言某个业务元素出现了。8.3 中文环境下的编码问题Windows 下跑 pytest有时候控制台输出中文乱码或者写日志时文件报编码错误。解决办法有两处一是 PyCharm 里把 File Encoding 设置为 UTF-8二是在代码文件头部显式声明# -*- coding: utf-8 -*-同时日志文件的 handler 用encodingutf-8。这些细节在 Linux CI 环境一般不会出现但在本地开发时很坑。8.4 依赖冲突和虚拟环境迁移我从一个项目切换到另一个项目时也曾经因为两个项目使用了不同版本的 Selenium 和 Requests导致一个项目跑不起来。现在我的习惯是每个项目必须有独立的 venv 和requirements.txt换电脑或换同事机器时用一条命令就能重现环境。如果项目里有十几个依赖包我还会用pip freeze requirements.txt生成锁定版本的依赖列表能避免很多我这边明明能跑的扯皮。8.5 对测试报告要有反应最后一个坑不是技术问题而是流程问题。自动化测试跑完之后如果没人看报告、没人处理失败用例那这套自动化等于白做。我见过好几个团队辛辛苦苦搭了框架但 Jenkins 上的任务已经红了两个星期没人管最后老板一问大家说应该是环境问题。所以搭建自动化测试的同时一定要定好规则失败用例当天必须有人分析能修复的修复不能修复的要写明原因并跟踪否则自动化测试很快沦为无人问津的摆设。我自己带新人的时候经常说自动化测试入门最快的方式不是看视频而是挑一个真实项目里最核心的流程比如登录、下单、支付从接口自动化开始搭然后接 UI最后接报告和 CI。跑完这一整个流程你对 Python 自动化测试的全貌基本就有数了。剩下的就是在实践中不断积累那些文档里查不到的经验。比如你现在看到这最后几节就是我从连续三周在 Jenkins 上修脚本的日子里总结出来的希望你不用再把同样的时间浪费一遍。