Python自动化测试完整指南:接口、UI、数据驱动与持续集成

Python自动化测试完整指南:接口、UI、数据驱动与持续集成 做自动化测试这行久了经常被问到同一个问题“到底怎么用 Python 写自动化测试”问的人有刚转行的测试新人也有写了好几年功能用例但想突破的老手。我发现大家普遍不缺学习热情缺的是一个能从零讲清楚、又带真实代码的思路梳理。网上资料很碎要么只讲 pytest 怎么用要么只讲 selenium 怎么点页面很少有人把“自动化测试到底是什么、该测哪些层、代码怎么写”串成一条线。所以这篇文章我把自己的实战经验整理出来围绕 Python 自动化测试完整跑一遍从整体思路、环境搭建、pytest 框架、接口测试到 UI 自动化、数据驱动、测试报告再到持续集成和排坑指南。每一步都配了能直接复制运行的代码和说明。不管你之前有没有接触过自动化只要按这个路线走都能搭出一套属于自己的测试体系。项目本身是我们团队最近重构的一个登录模块我拿它作为贯穿全文的案例。1. 自动化测试的核心思路与方案选型1.1 先想清楚你到底要自动化什么很多人一上来就学 selenium 模拟浏览器点击这是最大的误区。自动化测试不是“用工具代替手工点按钮”而是把你的测试逻辑沉淀成可以反复执行的代码资产。动手之前先搞清楚你被测的对象是什么形态。拿我们团队这个登录模块来说它表面看是一个 Web 页面但你把它拆开会得到三层底层是登录接口输入用户名、密码返回 token 和用户信息。中间层是前端页面用户填表单、点按钮、看到登录成功或失败提示。上层还有各种异常场景密码错误、用户不存在、账号锁定、验证码过期、并发登录。如果一上来就盯着页面去写 selenium 用例你会发现两个痛点页面刚改版脚本就全挂而且异常场景很难全部通过 UI 覆盖。所以我在项目里一直推行“分层测试”的思路——能用接口测的逻辑绝不上 UIUI 只负责验证页面交互本身。这个思路不仅让脚本稳定了很多维护成本也降到一个测试同学能扛住的程度。1.2 Python 语言与框架选型的理由选 Python 做自动化老实说不是因为 Python 性能强而是它有三个其他语言比不了的优势第一是生态。pytest、requests、selenium、appium 这些库全是现成的遇到问题搜一下基本都有答案社区成熟到你想踩个没人踩过的坑都难。第二是语法简单。团队里其他同学即使没写过代码读 Python 用例也能猜个大概降低了协作门槛。第三是调试方便。Python 的交互式环境、pdb、断点调试配合 IDE 用起来非常顺手写测试用例时效率很高。框架方面我在不同项目和阶段用过几种方案框架适用场景我的使用感受unittestPython 自带无需安装适合老项目或要求零依赖的场景但写起来较啰嗦pytest大部分项目的首选断言简洁fixture 强大插件丰富我现在的主力robotframework关键字驱动偏业务向适合测试团队中业务人员较多的场景灵活性稍差behave行为驱动开发BDD适合需要业务方参与评审用例的项目最终选 pytest因为它既满足断言简洁、参数化好写又支持 conftest.py 共享夹具、allure 报告插件、jenkins 集成等能力。对多数团队来说pytest 可以一套用到底从几小时的脚本到大型项目都不需要换框架。1.3 一条清晰的自动化测试路线基于上面的思考我建议新人按下面的优先级推进先把接口测试做起来。接口稳定性是业务稳定的基础而且接口用例执行速度快反馈及时。再用 pytest 把接口用例组织成可维护的框架覆盖正常路径和异常路径。最后才补 UI 自动化重点覆盖核心流程和跨系统跳转不要追求全页面覆盖。配合数据驱动、测试报告、持续集成把自动化纳入日常迭代。这样的路线不会让你一上来就被浏览器驱动、页面定位这些问题拦住而是在短时间内产生实际价值逐步建立信心。2. 环境准备与基础工具链新手先看这里2.1 安装 Python 并创建独立虚拟环境如果你已经装过 Python可以跳过安装这一步但虚拟环境建议认真配置。我见过太多人图省事把依赖全装到全局环境结果项目一多依赖版本互相冲突到时候拆环境比写测试还痛苦。第一步检查 Python 版本。Windows 打开命令行输入python --versionmacOS/Linux 可能需要用 python3python3 --version建议使用 Python 3.9 及以上版本太老的版本对 pytest、pydantic 等新特性支持不友好。第二步为当前项目创建虚拟环境。以项目目录 login_test 为例mkdir login_test cd login_test python -m venv venvWindows 激活虚拟环境venv\Scripts\activatemacOS/Linux 激活source venv/bin/activate看到命令行前面出现(venv)就说明激活成功。之后所有依赖都安装在这个环境里删除整个目录也不会影响系统全局环境。这个习惯我在所有项目里都保留推荐你也养成。2.2 安装 pytest、requests、selenium 等核心依赖依赖安装我只用 pip简单直接。把下面命令依次执行pip install pytest pip install requests pip install selenium pip install pytest-html pip install allure-pytest pip install webdriver-manager这里webdriver-manager尤其值得推荐。以前用 selenium 最烦的就是手动下载浏览器驱动还要匹配版本号换了台机器就得重新搞。用了它以后驱动会自动下载并匹配当前浏览器团队里谁也不用再为驱动版本折腾了。安装完可以验证一下pytest --version如果能正常显示 pytest 版本号说明环境就绪了。2.3 项目结构规划简单但清晰一个良好的项目结构决定了脚本后期好不好维护。我习惯采用下面的目录划分login_test/ ├── venv/ # 虚拟环境 ├── pages/ # 页面对象层 │ └── login_page.py ├── testcases/ # 测试用例层 │ ├── test_login_api.py │ └── test_login_ui.py ├── data/ # 测试数据和配置文件 │ ├── testdata.json │ └── config.ini ├── reports/ # 测试报告输出目录 ├── conftest.py # pytest 共享夹具 └── requirements.txt # 依赖清单先把依赖清单导出来方便以后换环境复原pip freeze requirements.txt以后别人拿到这个项目直接执行pip install -r requirements.txt就能装好依赖不用一个个问“你装了哪个包”。3. 接口自动化测试用 pytest 写登录模块用例3.1 了解被测接口登录模块的接口我们简化一下假设是一个 RESTful APIPOST /api/login 请求体{username: admin, password: 123456} 成功响应{code: 0, message: success, data: {token: abc123token}} 失败响应{code: 1001, message: 用户名或密码错误, data: null}接口测试的要点是关注返回值而不是页面长什么样所以写起来非常直接。先确认 URL、请求方法、请求头、请求体这些基本信息再用代码发起请求即可。3.2 先用 requests 手动验证一次不要一上来就写测试框架先用脚本把接口调通确认参数没问题。这样后面写用例时报错了你才知道是环境问题还是代码问题。import requests url http://127.0.0.1:8080/api/login payload { username: admin, password: 123456 } headers { Content-Type: application/json } resp requests.post(url, jsonpayload, headersheaders) print(resp.status_code) print(resp.json())如果看到响应里有“code”: 0和 token 字段说明接口通了。这一步验证非常关键我见过不少同学直接跳到框架层写代码最后发现是自己的接口地址写错了白白排查很久。3.3 把接口请求封装成公共方法每次用例里都写 requests.post 会很啰嗦而且接口地址一改所有用例都要改维护成本很高。我习惯封装一个专门的请求客户端。# utils/api_client.py import requests class ApiClient: BASE_URL http://127.0.0.1:8080 def __init__(self, tokenNone): self.session requests.Session() self.token token if token: self.session.headers.update({Authorization: fBearer {token}}) def post(self, path, **kwargs): url self.BASE_URL path return self.session.post(url, **kwargs) def get(self, path, **kwargs): url self.BASE_URL path return self.session.get(url, **kwargs)这样写的好处是token 可以在登录后统一注入请求头、基础地址都集中管理后面用例代码会非常干净。3.4 pytest 用例覆盖正常与异常路径一个登录接口至少要有以下用例正确的用户名和密码能返回 token。密码错误返回错误码 1001。用户不存在返回错误码 1002。参数缺少 username返回参数校验错误。用 pytest 写出来是这样# testcases/test_login_api.py import pytest from utils.api_client import ApiClient class TestLoginApi: def setup_method(self): self.client ApiClient() def test_login_success(self): resp self.client.post(/api/login, json{ username: admin, password: 123456 }) result resp.json() assert result[code] 0 assert token in result[data] def test_login_wrong_password(self): resp self.client.post(/api/login, json{ username: admin, password: wrong }) result resp.json() assert result[code] 1001 assert result[message] 用户名或密码错误 def test_login_user_not_exist(self): resp self.client.post(/api/login, json{ username: not_exist_user, password: 123456 }) result resp.json() assert result[code] 1002 def test_login_missing_username(self): resp self.client.post(/api/login, json{ password: 123456 }) result resp.json() assert result[code] 1003 assert username in result[message]写到这里注意一点断言一定要落到具体字段不要只断言resp.status_code 200。HTTP 状态码只能说明请求成功不能代表业务成功。业务是否成功要看业务返回码和关键数据。运行用例pytest testcases/test_login_api.py -v看到每个用例都 pass接口测试部分就完成了。这里我先不引入过多抽象因为接口测试的第一要务是直白、可读后续再逐步优化。4. UI 自动化测试用 selenium 覆盖登录页面关键流程4.1 UI 自动化该测什么很多团队把 UI 自动化当成“全自动回归”的万能钥匙结果脚本数量和崩溃概率成正比。我个人的原则是UI 只测那些必须验证用户真实交互的场景。登录模块的 UI 用例我聚焦在页面元素是否正常显示用户名输入框、密码输入框、登录按钮、错误提示。输入错误密码是否出现明确提示且不会跳转成功。输入正确账号密码是否跳转首页。空值校验点击登录不提交并提示必填项。至于密码加解密、token 过期、并发登录这类逻辑放接口层测试更合适。毕竟 UI 上你根本看不到这些细节。4.2 写一个简单的页面对象selenium 写久了最怕定位信息散落在各个用例里。今天改个 name 属性明天改个 id你就要在几十个文件里搜替换。我一般用页面对象模式把页面上的元素和操作集中到一个类中。# 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 # 等待时间统一设置为 10 秒 self.wait WebDriverWait(driver, 10) # 定位器的集中管理改样式时只要改这里 username_input (By.ID, username) password_input (By.ID, password) login_button (By.ID, loginBtn) error_tip (By.CLASS_NAME, error-tip) success_text (By.CLASS_NAME, welcome) def login(self, username, password): self.wait.until(EC.visibility_of_element_located(self.username_input)).send_keys(username) self.wait.until(EC.visibility_of_element_located(self.password_input)).send_keys(password) self.wait.until(EC.element_to_be_clickable(self.login_button)).click() def get_error_tip(self): return self.wait.until(EC.visibility_of_element_located(self.error_tip)).text def get_success_text(self): return self.wait.until(EC.visibility_of_element_located(self.success_text)).text这里有两个细节很关键。第一不要用time.sleep()。写 UI 自动化的人最开始都用 sleep页面没加载完就 sleep 三秒sleep 完了还没加载完就 sleep 五秒。到最后用例跑一次要十分钟其中九分钟都在睡觉。我改用WebDriverWait加预期条件元素出现就继续没有白白等待用例速度快稳定性也显著提升。第二定位方式的优先级。我一般优先用 id其次用 name再其次用 CSS 或 XPath。class 名如果含有动态变化就不要用了XPath 如果涉及到多层嵌套也尽量精简以免页面微调就失效。4.3 在 pytest 中驱动浏览器执行用例要跑 selenium 用例得先启动浏览器驱动。我用 webdriver-manager 来自动管理驱动。# conftest.py import pytest from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from pages.login_page import LoginPage pytest.fixture def driver(): service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice) driver.maximize_window() driver.implicitly_wait(5) yield driver driver.quit() pytest.fixture def login_page(driver): page LoginPage(driver) driver.get(http://127.0.0.1:8080/login) return page这里driver是浏览器实例的夹具login_page依赖driver实例后自动打开登录页。pytest 会按依赖顺序执行用完自动关闭浏览器每个用例之间浏览器状态互不影响。然后写用例# testcases/test_login_ui.py def test_login_success(login_page): login_page.login(admin, 123456) assert 欢迎回来 in login_page.get_success_text() def test_login_wrong_password(login_page): login_page.login(admin, wrong) assert 用户名或密码错误 in login_page.get_error_tip() def test_login_empty_username(login_page): login_page.login(, 123456) assert 用户名不能为空 in login_page.get_error_tip() def test_login_empty_password(login_page): login_page.login(admin, ) assert 密码不能为空 in login_page.get_error_tip()这三个用例覆盖了正常登录、错误密码、空值校验已经足够作为 UI 自动化的入门样例。如果你项目里登录后跳转要时间较长可以继续在成功断言前追加一个显式等待确保目标页的元素加载完成。5. 数据驱动与参数化让用例飞起来5.1 为什么需要数据驱动手动维护一堆测试用例函数有一个痛点新增一组测试数据就要复制一个函数不仅代码冗余而且看用例列表时得翻好久。pytest 的参数化功能就是专门解决这个问题的。拿登录接口来说我可以把所有“异常密码”的测试数据放到一个列表里一条用例函数跑多组数据报告里每条数据算作一个独立的用例结果。import pytest from utils.api_client import ApiClient pytest.mark.parametrize(username,password,expected_code,expected_msg, [ (admin, wrong1, 1001, 用户名或密码错误), (admin, 12345_xxx, 1001, 用户名或密码错误), (guest, 123456, 1002, 用户不存在), (, 123456, 1003, 用户名不能为空), (admin, , 1003, 密码不能为空), ]) def test_login_invalid_params(username, password, expected_code, expected_msg): client ApiClient() resp client.post(/api/login, json{ username: username, password: password }) result resp.json() assert result[code] expected_code assert expected_msg in result[message]这样一组数据就是一条用例方便维护也方便看报告里到底哪一条测试数据挂了。如果你有大量数据还可以把这些数据放在独立的data/testdata.json文件里用 pytest 的 hook 读取并传入做到数据与代码完全分离。5.2 pytest 的 fixture 使用心得数据驱动只是冰山一角pytest 真正的威力在于 fixture。fixture 可以帮你做很多事提供测试数据比如构造一个已经登录的 token。做前置准备比如清空数据库中的脏数据。做后置清理比如删除测试生成的临时文件。实现不同权限用户的复用比如准备一个普通用户和一个管理员用户。举个例子登录后获取 token 是一个很常见的依赖。我可以写一个 fixtureimport pytest from utils.api_client import ApiClient pytest.fixture(scopesession) def user_token(): client ApiClient() resp client.post(/api/login, json{ username: admin, password: 123456 }) token resp.json()[data][token] return token pytest.fixture(scopesession) def admin_token(): client ApiClient() resp client.post(/api/login, json{ username: admin_root, password: admin_pass }) token resp.json()[data][token] return tokenscopesession表示整个测试会话只执行一次而不是每个用例都重新登录能显著缩短测试时间。如果每个用例需要独立的用户状态再把 scope 改为function。fixture 还可以组合使用。比如某些接口需要同时带 token 和管理员标识我就在用例参数里同时声明user_token和admin_tokenpytest 会自动注入。5.3 用 conftest.py 共享夹具如果多个测试文件都要用到user_token你不用在每个文件里重复定义 fixture把它放进项目根目录的conftest.py即可。pytest 会自动发现并注入到所有子目录的测试用例中。我在实际项目里的习惯是根目录 conftest.py 放全局共享的 fixture比如浏览器、数据库连接、日志配置。每个子测试目录下设自己的 conftest.py放只跟该目录相关的夹具。不要把所有逻辑都堆在 conftest.py 里否则后期这个文件会膨胀到看不过来的程度。6. 测试报告、配置文件与日志把测试跑得明明白白6.1 生成 HTML 测试报告测试跑完不能只是控制台里刷一片绿。你要给团队一份看得懂的报告里面写清楚跑了多少条、过了多少条、挂了多少条、挂在哪个用例上。pytest 最常用的两个报告插件是 pytest-html 和 allure-pytest。pytest-html 简单直接一条命令就出报告。pytest testcases/ --htmlreports/report.html --self-contained-html--self-contained-html参数会把 CSS 和 JS 都嵌入 HTML 文件里方便直接传给别人看。如果项目比较大我建议用 allure。allure 报告的交互性和信息量更强点击一个用例能看到贴图、日志和层级关系。pytest testcases/ --alluredirreports/allure-results allure serve reports/allure-results运行后会自动拉起本地服务展示报告。allure 还支持在用例里添加描述和团队分组信息import allure allure.feature(登录模块) allure.story(用户登录) allure.title(正确账号密码可以登录成功) def test_login_success(login_page): with allure.step(输入账号密码): login_page.login(admin, 123456) with allure.step(校验提示信息): assert 欢迎回来 in login_page.get_success_text()这样报告中会把操作步骤、断言结果展示得非常直观即使不看代码也能明白整个测试在做什么。对测试新手来说这是最容易体现专业度的地方。6.2 配置文件管理环境地址环境地址、账号密码这类信息最好不要硬编码在脚本里。测试环境、预发布环境、生产环境的地址是不同的我一般用config.ini或.env文件管理。以 config.ini 为例[api] base_url http://127.0.0.1:8080 login_path /api/login [browser] headless true timeout 10读取配置可以直接用 Python 内置的 configparser# utils/config.py import configparser import os config configparser.ConfigParser() config.read(os.path.join(os.path.dirname(__file__), ../data/config.ini)) def get_api_url(): return config[api][base_url] def get_browser_timeout(): return int(config[browser][timeout])做到这一步换环境测试时只需要改配置文件脚本一行都不用动。我见过不少团队把测试环境地址写死在用例里每次切环境都要全局替换非常痛苦。6.3 日志配置别等失败才后悔自动化测试跑挂的时候你拿到一堆报错信息却不知道当时页面显示什么、接口返回什么。这时如果脚本里有日志记录排查效率会高非常多。我习惯在 conftest.py 里加一个日志配置# conftest.py import logging import pytest pytest.fixture(scopesession, autouseTrue) def setup_logging(): logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(reports/test.log, encodingutf-8), logging.StreamHandler() ] ) yield然后在用例的关键动作处记录日志import logging logger logging.getLogger(__name__) def test_login_success(login_page): logger.info(开始执行登录成功用例) login_page.login(admin, 123456) text login_page.get_success_text() logger.info(f页面提示: {text}) assert 欢迎回来 in text配合 allure日志会直接展示在测试步骤中问题发生点是哪一步一目了然。UI 自动化还可以在断言失败时自动截图你就连现场都有了。这个后面在常见问题部分再细节补充。7. 常见问题与排查技巧实录7.1 元素定位不到优先检查这三点selenium 报NoSuchElementException是新手最常遇到的错误。我以前排查时走了不少弯路总结了下面这个检查顺序页面是否真的加载到了如果页面跳转没那么快需要显式等待元素出现。优先把WebDriverWait用起来而不是单纯指望隐式等待。元素是否在 iframe 里在 iframe 里需要先switch_to.frame()再定位否则永远找不到。定位器是否写得能唯一定位如果页面有多个相同 class 的元素比如都是button那么用 XPath 时要加上前置条件比如//button[typesubmit]。定位成功以后最好再做一次断言验证确认你定位到的确实是你想要的元素而不是页面上的某个同名元素。这个习惯能省掉不少莫名其妙的“操作成功但结果错误”的问题。7.2 浏览器驱动版本不匹配以前用 selenium 最头疼的问题之一就是SessionNotCreatedException提示 driver 版本和浏览器版本不兼容。方案有两个一是用 webdriver-manager让它自动下载匹配的驱动from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)二是如果公司网络有限制手动下载驱动放到指定目录并用serviceService(驱动路径)指定。推荐把驱动路径写进配置文件方便团队共享。个人建议优先用 webdriver-manager虽然初次运行会下载驱动但后续自动化程度高不用每次手动管理。7.3 接口测试超时或 404怎么定位接口测试挂掉的原因主要集中在三块环境没起来服务返回 connection refused。URL 写错了返回 404。请求体格式不对接口返回 500 或参数校验异常。我的排查方法很简单先用 Postman 或 curl 手动调一次接口看能不能通。如果 Postman 能通脚本不通基本就是脚本里请求头、请求体或者路径写错了。如果 Postman 也不通那就是环境或服务本身的问题。还有一点容易忽视接口返回了乱码或 JSON 解析失败很可能是编码问题。可以在请求时指定编码或者在读取响应时做异常处理。7.4 用例间相互影响测试用例最忌讳互相依赖。a 用例执行成功b 用例才能执行这种设计会让问题排查变得痛苦。a 挂了b 也挂但 b 实际没有问题。在设计用例时我遵循“用例独立性”原则每个用例自己准备数据执行完自己清理。不要依赖上一个用例的登录状态。操作浏览器时每个用例启动全新的浏览器实例。在接口测试层面我通常在 fixture 里构造独立的测试数据测试结束后清理掉避免影响下一次运行。7.5 失败重试与自动截图用例偶尔会因网络抖动、前端渲染慢而失败。这种不稳定对回归测试来说是噪音会干扰你定位真实问题。我给团队的方案是加上失败重试机制并在 UI 失败时自动截图。pytest 可以通过 pytest-rerunfailures 插件实现失败重试pip install pytest-rerunfailures运行命令时加上参数pytest testcases/ --reruns 2 --reruns-delay 5UI 测试可以在 fixture 里加失败截图功能pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs.get(driver) if driver: screenshot_path freports/{item.name}_{int(time.time())}.png driver.save_screenshot(screenshot_path) print(f失败截图已保存: {screenshot_path})这样每次失败都会留下现场图片定位问题就直观多了。不过要注意重试只适合处理偶发的不稳定问题如果用例本身逻辑有 bug重试多少次都会失败得老老实实改代码。7.6 把自动化测试接入持续集成自动化脚本只在本地跑价值有限。真正体现价值的时候是每次代码提交、每次构建都自动跑一遍并把结果通知到团队。我把这套流程接入了 Jenkins核心步骤很简单安装 Python 环境。拉取代码。安装依赖。执行 pytest。发布报告。发送通知。Jenkins 的构建命令大致如下python -m venv venv source venv/bin/activate pip install -r requirements.txt pytest testcases/ --htmlreports/report.html --self-contained-html如果对容器技术熟悉用 Docker 构建一个带 Python 和 Chrome 的镜像跑脚本会更稳定能让测试环境始终一致避免“在我电脑上能跑”的尴尬。8. 关于这次实测项目的复盘与延伸这次拿登录模块做完整示例是因为它的业务逻辑清晰接口和 UI 界限分明非常适合作为自动化测试入门的案例。从接口测试到 UI 测试再到数据驱动和报告这其实是一条完整的从零到一搭建自动化测试的过程。我个人的心得是自动化测试不是为了炫技术而是为了让人从重复劳动中解脱出来。真正落地的自动化框架前提是稳定、可维护、可追溯。这要求你对自己被测系统有足够深入的理解而不是只会套模板。如果你是从零开始建议不要一上来就追求大而全的框架先把你日常手工回归里最频繁验证的那几条用例跑通一个小项目的价值可能比一个完美框架大得多。等积累够了再往数据驱动、CI 集成这些方向延展。最后再分享一个我一直在用的小技巧给团队定一个“红黄绿”规则——每次提交代码自动触发一套核心用例全绿才可以合并偶尔失败但重跑后通过说明存在不稳定因素必须记录并跟踪连续多次失失败且无法重跑通过必须停下发布优先定位真实问题。这样测试结果就不再是“看起来有测试”而是真正成为质量的一道闸门。自动化测试这条路上没有捷径但方向对了每一步都会让你越来越轻松。