Python自动化测试与CI/CD落地:从环境搭建到稳定性治理 说实话很多人一提到Python自动化测试第一反应就是装个Selenium写个脚本把网页从头点一遍然后就没有然后了。结果呢本地跑得好好的换台机器就报错用例写了两百条一到回归就红成一片CI/CD听起来很高大上可真把测试接进流水线反倒成了最不稳定的那一环。这还真不是技术不够而是缺少一条从环境、用例设计、框架选型到流水线集成的完整链路。这篇文章就按这条链路来写内容覆盖Python自动化测试与CI/CD落地既有接口自动化也有UI自动化还会专门讲几类高频踩坑点比如非预期弹窗导致的失败、CI环境不稳定、报告没人看之类的问题。你如果已经能写出“能跑”的Python脚本但不知道自动化测试该怎么工程化或者刚被安排去搭建测试体系看这篇应该能少走不少弯路。1. 自动化测试起步先把Python工程环境搭成“能长期用”的样子很多人一上来就pip install selenium然后开写写到第三个项目就崩了包版本打架、Python版本对不上、别人拉你代码跑不起来。我见过太多这种场景所以第一件事不是讲框架而是把工程环境理清楚。1.1 Python版本和虚拟环境怎么选Python版本我用得最多的是3.9到3.11这个区间。别一上来就追最新版企业项目里很多依赖库还没跟上你装个3.13Selenium或某些内部封装库可能就报兼容性问题。同样也不要还停留在2.7时代处理中文编码和依赖管理会让你怀疑人生。虚拟环境是必选项。每个项目一个独立的解释器环境依赖版本互相隔离这个习惯越早养成越好。打个比方你不可能把装修用的工具箱和做饭用的调料盒放在同一个抽屉里项目也是一样A项目要Django 3.2B项目要Django 4.2放一起必出事。创建虚拟环境的路径很简单# 进入项目目录 cd my_automation_project # 创建虚拟环境venv是Python自带的不需要额外安装 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS / Linux: source venv/bin/activate # 看到命令行前面出现(venv)就说明激活成功了激活后安装的所有包都会进到这个虚拟环境里。依赖导出我建议分场景如果有完整的依赖清单直接用requirements.txt如果项目一开始没规划好想自动找出实际用到的包可以试试pipreqs。最简单的方式是这样的pip freeze requirements.txt不过你如果环境里已经装了很多不相关的包freeze会把它们全部打进去。我更建议定期整理只保留项目实际使用的依赖这样你或者别的同事用pip install -r requirements.txt的时候不会装一堆用不上的东西。1.2 目录结构提前定好能省很多事自动化测试项目最怕的就是所有脚本堆在一个目录里。今天一个login_test.py明天一个test_order.py全在根目录过两个月看到就头疼。我常用的自动化测试项目结构是这样my_automation_project/ ├── config/ # 配置文件和公共常量 │ ├── __init__.py │ └── settings.py ├── data/ # 测试数据文件Excel、YAML、JSON都放这 ├── tests/ # 测试用例 │ ├── __init__.py │ ├── test_api/ # 接口用例 │ └── test_ui/ # UI用例 ├── common/ # 公共封装如请求封装、日志封装、弹窗处理 │ ├── __init__.py │ └── request_util.py ├── reports/ # 测试报告和日志输出 ├── requirements.txt └── pytest.ini # pytest配置pytest.ini里我会至少配置这样几个字段[pytest] testpaths tests log_cli true log_cli_level INFO addopts -v --tbshort配置了testpaths之后pytest会自动去tests目录下收集用例不用每次手动指定路径。log_cli开启后跑用例的时候能直接看到日志输出排查问题会舒服很多。1.3 VSCode配置Python环境的两个细节用VSCode写Python是现在的主流但很多人会遇到同一个问题明明在终端里能import的包编辑器却报红或者调试的时候提示ModuleNotFoundError。原因基本都是解释器没选对。在VSCode里按CtrlShiftP输入“Python: Select Interpreter”选择你虚拟环境里的那个python.exe而不是系统全局的Python。选完重启一下终端再跑代码就正常了。另一个细节是调试配置。用pytest跑用例不需要自己去写一堆main函数直接在VSCode的调试面板里创建launch.json配置成pytest即可{ version: 0.2.0, configurations: [ { name: Python: pytest, type: python, request: launch, module: pytest, args: [-v] } ] }这样你可以在用例里打断点点调试就进pytest流程观察变量和调用链比print大法效率高得多。2. 接口自动化测试从“能调通”到“能回归”的完整思路UI自动化不稳定是个长期痛点所以在项目初期优先做接口自动化是性价比最高的方案。接口测试跑得快、定位准接口过了前端大概率不用背锅接口挂了问题范围直接缩小到后端开发排查也快。2.1 为什么接口自动化要放在UI前面测试金字塔很多人都听说过底层大量单元测试中间层服务/接口测试顶层少量端到端UI测试。落到实际业务里我的经验是接口自动化至少要占自动化用例的60%以上。接口测试的好处是稳定。UI上每个版本样式变一变元素定位可能就全崩了但接口的字段和协议相对稳定。而且接口用例执行速度极快一个后端接口用例几十毫秒到几百毫秒就出结果UI用例动辄几十秒甚至一分钟起步。跑完100个接口用例可能只要2分钟100个UI用例可能要跑一小时这个时间成本在CI/CD里差异会被无限放大。2.2 requests核心用法别把写请求当调工具Python做接口自动化requests库是绝对的主角。但很多人只是会用requests.get()、requests.post()真正遇到问题就懵了。先看一个实际场景接口需要先登录拿token然后带着token去请求业务数据。import requests # 登录接口 login_url https://api.example.com/login login_data { username: tester, password: 123456 } resp requests.post(login_url, jsonlogin_data, timeout10) assert resp.status_code 200 token resp.json()[data][token] # 带token请求业务接口 headers {Authorization: fBearer {token}} order_url https://api.example.com/orders order_resp requests.get(order_url, headersheaders, timeout10) assert order_resp.status_code 200 order_data order_resp.json()这里有几个细节值得注意用json传参requests会自动把字典序列化成JSON并且设置Content-Type为application/json。如果你用data传参发送的是表单格式很多后端接口根本解析不了。timeout一定要设置。不设timeout的话接口如果一直没有响应你的测试用例会一直挂在那里在CI上直接拖垮整个流水线。10秒是我常用的默认值取决于业务接口的响应时间可以调大/调小。每一个请求都要做响应断言不能只print出来人眼观察否则自动化失去了意义。更复杂的场景比如需要保持登录状态requests.Session可以帮你自动维持Cookie和连接池session requests.Session() session.post(login_url, jsonlogin_data) # 后续请求直接用session.get/post自动带登录状态 resp session.get(order_url, timeout10)2.3 pytest组织用例fixture和参数化是核心requests负责发请求pytest负责把用例组织起来。如果你还在用unittest我强烈建议切到pytest语法更简洁fixture和参数化也更灵活。fixture适合处理“每个用例都需要的前置条件”import pytest import requests pytest.fixture(scopesession) def base_url(): # 这里可以做环境切换qa/prod return https://api.example.com pytest.fixture(scopesession) def login_session(base_url): s requests.Session() resp s.post( f{base_url}/login, json{username: tester, password: 123456}, timeout10 ) return s def test_get_orders(login_session, base_url): resp login_session.get(f{base_url}/orders, timeout10) assert resp.status_code 200 assert resp.json()[code] 0fixture的scope参数很有讲究scopefunction每个用例都执行一次适用于创建临时数据。scopesession整个测试会话只执行一次比如登录获取token复用在多个用例里。参数化是做数据驱动的核心。接口测试经常要覆盖多组输入比如不同账号、不同参数组合import pytest pytest.mark.parametrize(username,password,expected_code, [ (tester1, 123456, 0), (tester2, wrong_pwd, 10001), (, 123456, 10002), ]) def test_login_cases(username, password, expected_code): resp requests.post( https://api.example.com/login, json{username: username, password: password}, timeout10 ) assert resp.json()[code] expected_code这种写法有三个作用一是用例简洁一组参数就是一条测试场景二是可读性好后面的人一看就知道覆盖了哪些分支三是新增场景成本极低往参数列表里加一行就行。更复杂的场景可以把数据放到YAML或Excel里然后写一个读取数据的工具类配合pytest生成用例。不过刚开始做不建议一上来就搞太重的数据驱动框架先用参数化把核心场景跑通后面再逐步扩展。2.4 环境切换和动态参数最容易踩的两个坑接口自动化项目做大了一定会遇到环境问题。本地联调用测试环境流水线里也要跑不同环境代码里如果写死了域名换个环境就得改代码。我的做法是在pytest.ini里配置环境变量或者在config/settings.py里统一管理import os ENV os.getenv(TEST_ENV, qa).lower() BASE_URL_MAP { qa: https://qa-api.example.com, staging: https://staging-api.example.com, prod: https://api.example.com, } BASE_URL BASE_URL_MAP.get(ENV, BASE_URL_MAP[qa])然后在命令行里启动测试时指定环境TEST_ENVstaging pytest -q这样环境切换不碰代码流水线里也只需要改环境变量。动态参数是另一个高频问题。比如创建订单会返回一个订单ID然后需要拿这个订单ID去查订单详情。处理方式是把上一个接口的响应保存下来传给下一个接口。pytest里有一个非常方便的做法利用request fixture在用例之间传递数据但前提是同一个测试类里用例顺序是可控的import pytest pytest.fixture(scopeclass) def created_order(login_session, base_url): create_resp login_session.post( f{base_url}/orders, json{product: iphone, qty: 1}, timeout10 ) return create_resp.json()[data][order_id] pytest.mark.usefixtures(login_session) class TestOrderFlow: def test_get_order_detail(self, created_order, login_session, base_url): resp login_session.get(f{base_url}/orders/{created_order}, timeout10) assert resp.status_code 200这样设计的好处是创建订单这个“前置动作”只在每个测试类里执行一次然后多个用例可以复用而且用例之间的依赖关系在代码里一目了然。3. UI自动化测试的“脆皮”困境与合理取舍接口自动化解决的是业务逻辑的回归但有些交互流程比如注册、下单、页面操作光靠接口覆盖不了这时候必须上UI自动化。UI自动化要说最难的点不是写脚本而是处理稳定性和维护成本的问题。3.1 Selenium还是Playwright我的选型建议Selenium是老牌框架文档多、资料全、兼容性强团队的熟悉度高Playwright是后起之秀自带自动等待、录制脚本、多浏览器支持能处理登录页的复杂交互。我自己个人在做新项目的时候已经优先推荐Playwright了。对比维度SeleniumPlaywright安装复杂度需要额外下载driver版本需与浏览器匹配pip install playwright然后playwright install自动下载浏览器等待机制大量依赖WebDriverWait显式等待内置actionability等待元素不可交互时会自动等待调试工具需要自己截图、手动查HTML自带trace viewer可以回放步骤和查看网络请求多标签页/弹窗处理需要切换window_handles比较繁琐自带context/page模型弹窗处理相对简单学习成本低资料多中等但API更现代但这不意味着Selenium要被淘汰。如果你维护的是一个大规模Selenium项目完全没必要推倒重来稳定性和团队熟练度更重要。选型标准很简单新项目优先Playwright老项目继续用Selenium把稳定性做扎实。这里我以Selenium为例写一个UI登陆用例因为Selenium的资料最多很多团队还在用from selenium import webdriver 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(): driver webdriver.Chrome() driver.get(https://example.com/login) WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, username)) ).send_keys(tester) driver.find_element(By.ID, password).send_keys(123456) driver.find_element(By.ID, login-btn).click() WebDriverWait(driver, 10).until( EC.url_contains(/dashboard) ) assert /dashboard in driver.current_url driver.quit()3.2 定位与等待少用sleep多用显式等待UI自动化里90%的不稳定因素来自元素定位和等待。代码写快了元素还没渲染出来就去找网络慢了页面还在加载就去点击接口响应不确定sleep(5)可能不够也可能过多。元素定位优先级我建议这样排优先用data-testid、id、name这类稳定的属性前端开发可以配合预留测试钩子。其次用CSS选择器结构清晰性能好。XPath虽然强大但尽量少用绝对路径特别是含层级索引的前端稍微改个DOM结构定位就崩。等待机制一句话总结能用显式等待绝不用隐式等待能用等待绝不用sleep。显式等待的标准写法WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, order-list)) )系统会每隔几百毫秒检查一次元素是否出现最多等10秒。如果10秒还没出现就报超时异常这样效率比sleep高得多。把等待封装成公共函数避免每个用例里都写一堆样板代码def wait_element(driver, by, value, timeout10): return WebDriverWait(driver, timeout).until( EC.element_to_be_clickable((by, value)) )3.3 非预期弹窗导致失败一个值得单独解决的经典问题非预期弹窗被截获的弹出框是UI自动化最常遇到的失败原因之一。页面加载到一半突然弹出一个抽奖活动、一个服务通知、一个广告浮窗甚至一个系统级别的对话框直接把后续元素定位全挡住了用例当场失败。真实场景里弹窗类型基本有这几种浏览器原生弹窗alert/confirm/promptSelenium可以自动处理或用driver.switch_to.alert接受/关闭。页面内模态框Modal通常是一个div浮层用close按钮或遮罩层点击关闭。系统级弹窗比如下载、上传文件时的操作系统窗口处理起来更麻烦。处理思路不是每个用例都去写弹窗处理而是做一个统一防御。比如在公共的操作方法里每次点击前先尝试关闭可能出现的弹窗def safe_click(driver, by, value, timeout5): try: # 先尝试关闭常见的公告弹窗 close_btn driver.find_element(By.CLASS_NAME, close-popup) close_btn.click() except Exception: pass # 没有弹窗就算了不阻塞主流程 element wait_element(driver, by, value) element.click()更稳妥的办法是把弹窗处理和失败重试结合起来当用例因为点击失败而报错时先尝试恢复页面状态关闭弹窗、回到首页然后重试一次操作。如果重试还是不行再上报失败。def retry_click(driver, by, value, retry2): for i in range(retry): try: safe_click(driver, by, value) return except Exception as e: driver.save_screenshot(fretry_{i}.png) driver.refresh() # 刷新页面消除弹窗影响 raise AssertionError(f点击元素失败: {by}{value})这种“防御重试”的机制虽然看起来不够优雅但在实际项目中非常有效。它能硬扛掉很大一部分偶发的弹窗干扰让UI自动化真正在CI里稳定跑起来。3.4 无头模式与CI适配UI自动化接入CI服务器上通常没有图形界面所以必须开启无头模式。Selenium的配置如下from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headless) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--window-size1920,1080)--no-sandbox在部分Linux CI容器里必须加否则Chrome启动直接报错。--disable-dev-shm-usage容器环境/dev/shm空间太小不加可能导致Chrome崩溃。--window-size一定设大一点小窗口下很多前端组件会折叠或遮挡元素定位也会出问题。在CI里跑UI用例还有两个细节失败时一定要截图保存最好同时保存页面源码便于排查。跑完的后处理里用driver.quit()关闭浏览器不要等进程自己结束否则CI上会残留一堆僵尸Chrome进程最后把服务器内存耗光。4. 把测试代码放进CI/CD流水线从手动跑用例到自动发布前校验自动化测试项目跑通了下一步就是把它接进CI/CD。以前是人手动跑用例现在要变成代码提交后或每天固定时间自动跑并且结果自动通知给团队。4.1 为什么本地能跑流水线里就各种挂很多团队第一次把测试接进流水线都会遇到“本地是绿的CI上是红的”这种情况。原因无非这几类依赖版本不一致本地环境装了A包1.1.0CI里根据requirements.txt装的是A包1.0.9行为差异导致用例失败。环境变量缺失本地测试用的token、密钥写在代码里或环境变量里CI的机器上没有这些配置。网络或域名限制CI服务器可能访问不了某些内网域名或者访问外部环境超时。数据状态不一致本地数据库里有测试数据CI环境里的库是干净的或有脏数据导致接口返回值不同。解决思路就一条让CI环境尽可能贴近本地。通过Docker容器、固定依赖版本、在流水线里配置环境变量把环境和数据都前置准备好而不是等用例跑了才发现问题。4.2 一个可以直接抄的GitHub Actions配置如果你的代码托管在GitHub上GitHub Actions是最简单的方式。下面这个workflow演示了提交代码后自动拉代码、装依赖、跑pytest、上传报告全部自动完成name: Python Automation Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] schedule: - cron: 0 2 * * * # 每天凌晨2点跑一次回归 jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest pytest-html pytest-xdist pytest-rerunfailures pip install -r requirements.txt - name: Run tests env: TEST_ENV: staging API_TOKEN: ${{ secrets.API_TOKEN }} run: | pytest -n 4 --reruns 2 --htmlreports/report.html --self-contained-html - name: Upload report uses: actions/upload-artifactv4 if: always() with: name: pytest-report path: reports/这个配置文件里能改变命运的就是那三条GitHub Actions配置pytest -n 4用pytest-xdist并行跑4个进程用例多的时候速度能快好几倍。--reruns 2用pytest-rerunfailures对失败用例重试2次。注意重试的目的是容忍偶发网络抖动而不是掩盖真正的逻辑bug所以重试后依然失败的话流水线依然是红的。if: always()即使测试失败了也要上传报告文件方便排查。secrets.API_TOKEN这种敏感信息在GitHub仓库的Settings - Secrets里配置不会出现在代码里。4.3 Jenkins流水线怎么配置GitHub Actions好用但很多公司内网用的还是Jenkins。Jenkins里我习惯用PipelineJenkinsfile的方式把流水线配置写在代码仓库里方便版本化管理。一个简单示例如下pipeline { agent any environment { TEST_ENV staging PYTHON_BIN /usr/bin/python3 } stages { stage(Checkout) { steps { checkout scm } } stage(Setup) { steps { sh ${PYTHON_BIN} -m venv venv sh venv/bin/pip install -r requirements.txt } } stage(Test) { steps { sh venv/bin/pytest tests/ -n 4 --reruns 2 --alluredirallure-results } } stage(Report) { steps { allure includeProperties: true, jdk: , reportBuildPolicy: ALWAYS junit reports/*.xml } } } post { always { archiveArtifacts artifacts: allure-results/**, allowEmptyArchive: true } failure { // 发送通知钉钉/企业微信/邮件都行 emailext subject: 自动化测试失败: ${env.JOB_NAME}, body: 请查看流水线日志, to: qaexample.com } } }Jenkins配置里最有用的两个参数allure includeProperties直接生成可视化测试报告浏览器点开就能看比命令行的文本输出直观一百倍。archiveArtifacts把结果存档既有报告也有日志事后追溯方便。4.4 让CI执行更快、更稳定的两个技巧流水线跑多了就会发现除了用例本身的稳定性执行速度和稳定性同样影响团队情绪。如果每次跑一小时才出结果开发等不起如果流水线天天报错最后就没人看流水线状态了。执行速度优化手段在CI上做依赖缓存requirements.txt没有变化就不重复pip install。用pytest-xdist并行执行但注意用例之间不能有数据依赖否则并行会互相污染。把冒烟用例和全量回归用例分开。代码提交时只跑冒烟集合每天定时跑全量回归反馈速度快覆盖也到位。稳定性的核心手段失败重试。但只对UI用例和偶发性接口超时做重试不要掩盖真实失败。失败后的自动清理。用例执行中创建的测试数据无论成功失败都要清理避免脏数据影响后续运行。5. 跑得稳比跑得多重要稳定性治理与长期维护自动化测试跑起来了真正的挑战才刚开始。你会发现一个残酷的事实用例越多维护成本越高。如果自动化测试天天报红比不自动化还可怕因为团队会失去对它的信任。5.1 用例设计的独立性原则接口用例和UI用例每一类都应该尽量独立。意思是不管其他用例执行顺序如何、其他用例是否失败单个用例都应该能独立跑通。具体来说有三点用例的测试数据自己创建不要依赖之前的代码“刚好”创建了A数据。用例的前置准备用fixture或setup完成而不是在另一个用例里偷偷执行。不要在两个用例之间共享可变状态。比如A用例改了配置B用例假设配置还是原来的顺序一变就挂了。我刚开始写自动化的时候习惯把一些公共操作放在用例类里一个用例跑完数据没清理另一个用例直接拿这个数据继续跑。看起来省事了但后来发现这种用例完全没法并行、没法单独调试改起来也煎熬。后来全部改成独立用例虽然执行时重复创建了一些数据但稳定性和可维护性提升了好几个档次。5.2 减少“脆皮”用例的自我修养UI自动化里最怕的就是一类用例每次跑结果都不一样这次过了下次就挂了再跑一次又过了。这类“脆皮”用例特别消耗团队精力因为你不确定它是真bug还是假失败。脆皮用例的常见原因隐式等待不够或者用了固定sleep。解决办法是全部改显式等待。定位器写得太脆弱比如依赖了层级索引span[1]这种前端微调就挂。测试环境数据不稳定比如优惠券被领完了、订单状态被后续用例改了。浏览器执行动画未结束点击被浮动层拦截。解决办法是点击前判断元素是否可点击而不是简单判断存在。处理脆皮用例的关键是记录失败时一定要截图和保存页面源码看看到底卡在哪一步。如果连续多次同样的原因不要急着修先看是不是环境问题如果大概率是产品结构变了的偶发弹窗再决定是加防御还是改定位。5.3 自动化测试与手工测试边界在哪里有团队会问自动化测试都上了是不是手工测试就不需要了我的观点正好相反自动化替代的是那些重复、琐碎、执行频繁的回归步骤而探索性测试、需求理解、用户体验判断这些事自动化还替代不了。我的建议是用“风险优先”来做取舍核心主流程注册、登录、下单、支付优先自动化这些功能改动频繁、影响面大、回归成本最高。边缘条件和异常场景主要靠手工或者留一部分接口用例覆盖。新增功能的最初版本不建议急着自动化先把功能本身测稳定再补自动化否则你会整天改脚本而不是发现bug。自动化覆盖率不是越高越好重点是让测试体系能稳定运转。一个能一直跑、反馈准确的50条用例比一个三天两头红、没人会修的200条用例有价值得多。5.4 把自动化测试当成产品来维护这是我做了这么多年最深的体会。自动化测试项目和你写的任何产品代码一样有需求、有设计、有迭代、有维护。不是写完脚本就结束了而是把它当作一个长期存在的内部工具来运营每次业务功能变化时同步更新对应用例而不是等用例挂了才想起来改。定期清理无效用例不要留着那些永远在跑、永远不稳定的“僵尸用例”。对失败用例做分类统计是脚本问题、数据问题还是产品bug记录原因并持续优化。说一个具体的习惯我每两周会花一点时间拉出最近所有失败用例按原因归类。如果是元素定位变了当场就改如果是偶发环境问题加防御或重试如果是新需求改动了交互评估是否需要重写用例。把“维护用例”当成和“写代码”同样重要的事情来做自动化测试的长期价值才会体现出来。最后分享一个我自己的经验做自动化测试别急着一次铺开所有功能。选一条核心主流程把环境、用例、CI/CD、报告、通知全部打通让它能稳定跑一个月再考虑扩大范围。我见过不少团队一上来就规划几十个模块的自动化结果框架搭了三个月用例没跑几条项目组就被拖垮了。小而稳定先有个能给大家看的结果比什么都重要。