Python测试实战:pytest自动化与接口回归测试指南
做技术这些年最让我头疼的不是代码写不出来而是代码改完不知道会不会把老功能弄坏。尤其是刚接触Python那会儿写脚本全凭感觉今天改一个函数明天可能就把另一个模块带崩了。后来我养成了一个习惯任何代码改动先把测试跑一遍。Python测试这条路我从只会用print调试到跑通pytest自动化用例再到完成接口级回归测试中间踩过不少坑也总结出了一些实实在在的经验。这篇内容就围绕“Python测试实战”展开从环境准备到框架选型再从接口测试到报告落地尽量把我自己的操作路径完整还原出来。不管你是刚装完Python的纯新手还是已经能写业务代码、想提升代码质量的同学都应该能从里面找到能直接抄作业的部分。1. 测试基本功先把环境与思维理顺1.1 Python环境准备版本、虚拟环境与编辑器配置很多新手一上来就卡在环境上还没写一行测试代码先是Python装不上装了又启动不了。这里我直接给一套我实测下来最稳妥的配置方案。Python版本选3.11或3.12就行别追最新也别用太老的。安装时有个关键点一定要勾选“Add Python to PATH”。这个选项默认不勾如果漏了后面在命令行敲python会提示找不到命令相当于环境就废了一半。装好之后打开命令行验证一下python --version能看到版本号说明安装成功。接下来是虚拟环境。我见过太多人把项目依赖直接装到全局结果A项目需要requests 2.28B项目需要2.31两个一冲突就互相打架。虚拟环境就是为了隔离这种依赖冲突。创建和激活的命令如下python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate激活后命令行前面会出现(venv)标记这时候pip安装的所有包只会进当前项目。这步看起来多花了几秒钟但后续所有测试依赖都能被锁在项目内部单就这一点就少踩很多坑。编辑器方面vscode和pycharm都行。vscode装好Python扩展后按CtrlShiftP输入“Python: Select Interpreter”选择你刚才建好的venv路径。Pycharm则在Settings - Project - Python Interpreter里选中虚拟环境。编辑器配置这个动作虽然简单但经常有人漏掉导致明明装了pytest编辑器里却报“ModuleNotFoundError: No module named pytest”原因就是解释器还指着全局环境。1.2 测试金字塔先想清楚测什么环境就绪之后下一个问题不是“怎么写测试”而是“测什么”。这里我推荐脑子里先形成一张测试金字塔的图景底层是大量的单元测试中间是接口测试顶层是端到端的UI测试。用生活化的方式打个比方单元测试像检查每个零件能不能用接口测试像验证两个零件装在一起卡扣能不能合上UI测试则像整台机器通电后按钮按下去机器转不转。三层里单元测试成本最低、运行最快出了问题也最好定位UI测试最慢也最脆随便一个页面布局改动就能让十几个用例同时挂掉。所以合理的策略是把精力重点放在单元测试和接口测试上UI测试只挑核心流程写。1.3 自动化测试到底能解决什么问题有人会问我手动点几下不就行了为什么非要写自动化我个人体会是手工测试有两个致命问题一是回归成本高一个小项目改动后全量手点一遍半小时起步项目大了根本点不过来二是人容易漏刚改完某个模块觉得自己已经点过了其实另外一条路径根本没覆盖。自动化测试解决的就是“改动后快速确认没坏”这个核心需求。写完代码本地一键跑几百个用例几分钟出结果红了就知道哪里坏了。这个价值在刚入门时感受不明显等代码量上来了你会越来越觉得这是保命工具。2. 核心框架实战用pytest把测试写明白2.1 unittest与pytest到底选哪个Python自带unittest不用额外安装很多教程也拿它做例子。但我的建议是直接用pytest。原因有三个第一pytest的断言写起来更自然。unittest里要写assertEqual、assertTrue这一堆方法pytest直接一个assert搞定。第二pytest的fixture机制比unittest的setUp/tearDown好用太多。fixture可以跨文件共享可以按作用域自动执行和清理后面我会详细说。第三插件的生态完全不在一个量级。pytest-html生成报告、pytest-cov统计覆盖率、pytest-xdist并行跑用例这些基本是标配。安装pytest就一行命令pip install pytest2.2 第一个pytest用例断言、运行与常用参数新建一个test_demo.py文件写最简单的一个测试函数def add(a, b): return a b def test_add(): assert add(1, 2) 3 assert add(0, 0) 0在命令行执行pytest test_demo.py -v输出里会出现一个绿色或白色的点说明用例通过。这里的-pytest-命名规则是硬性的测试文件必须以test_开头或者以_test结尾测试函数必须以test_开头不然pytest默认找不到。断言这块有个经验要记牢先判断结果再写断言别把断言当成表达式的堆砌。比如浮点比较时0.1 0.2 0.3在Python里是False因为浮点数有精度问题。这时候应该用pytest.approximport pytest def test_float(): assert abs(0.1 0.2 - 0.3) 1e-9 # 或者直接用 assert 0.1 0.2 pytest.approx(0.3)还有异常断言。如果你测试一个函数是否正常抛异常直接写pytest.raisesdef divide(a, b): if b 0: raise ValueError(除数不能为0) return a / b def test_divide_zero(): with pytest.raises(ValueError) as exc_info: divide(1, 0) assert 除数不能为0 in str(exc_info.value)运行时推荐几个高频参数都是实测常用的pytest -v # 显示每个用例的详细结果 pytest -k add # 只运行名字包含add的用例 pytest -x # 第一个失败就停止快速定位 pytest --tbshort # 缩短错误堆栈不刷屏2.3 fixture机制测试前置条件和清理的利器fixture是pytest最强大的设计之一。看一个实际场景测试某个函数需要读取配置文件。你可以在每个测试里都写一遍配置读取逻辑但那样代码又臭又长。用fixture可以这样import pytest pytest.fixture def config_data(): data {host: 127.0.0.1, port: 8080} return data def test_config_port(config_data): assert config_data[port] 8080pytest会自动把config_data作为参数传入测试函数。这就是依赖注入的思路测试函数不需要自己准备数据只要声明需要什么fixture就给你造好什么。如果测试前后需要清理资源yield就派上用场了pytest.fixture def temp_file(): path /tmp/test_data.txt with open(path, w) as f: f.write(hello) yield path # 测试结束后自动删除文件 import os os.remove(path)记住yield后面那段代码在测试结束后一定会执行相当于自动帮你收拾现场。我个人的习惯是凡是涉及创建文件、连接数据库、开启服务的fixture都必须用yield做清理不然跑几次测试临时文件堆满目录会让你崩溃。多个测试文件里都要用的公共fixture可以放到conftest.py里。pytest会自动加载这个文件不需要你手动import。conftest.py是pytest约定俗成的公共配置文件名位置放在项目根目录整个项目都能用。2.4 参数化用一组数据跑多个用例有时候你有一个函数需要验证好几组输入输出。最笨的写法是复制粘贴多个test函数但那样用例一多就完全没法维护。pytest的parametrize就是解决这个问题的import pytest pytest.mark.parametrize(a,b,expected, [ (1, 2, 3), (0, 0, 0), (-1, 1, 0), (100, 200, 300), ]) def test_add_params(a, b, expected): assert add(a, b) expected这样跑完会看到4条用例每一条的名称后面会跟上参数值哪个组合失败一眼就能看出来。我这里也踩过一个坑参数一旦多了用例名称会变得特别长可读性差。解决办法是给参数化id加上说明pytest.mark.parametrize(a,b,expected, [ (1, 2, 3), (0, 0, 0), ], ids[正数相加, 零加零])2.5 测试项目的目录结构建议最后说下怎么组织测试文件。我的标准结构是这样project/ ├── src/ │ └── my_app/ │ ├── __init__.py │ └── core.py ├── tests/ │ ├── conftest.py │ ├── test_core.py │ └── test_api.py └── pytest.inipytest.ini里可以配置基础路径和参数比如[pytest] testpaths tests配置好后在项目根目录直接敲pytest它会自动扫描tests目录不用每次指定文件路径。3. 进阶实战接口测试与UI自动化3.1 接口测试的完整套路接口测试是自动化里头性价比最高的部分。为什么因为接口稳定不像UI那样一改版就全挂而且接口直接面对数据层业务逻辑对不对接口一测便知。先装依赖pip install requests pytest一个最简单的接口测试用例import requests def test_login(): url http://127.0.0.1:8000/api/login payload {username: admin, password: 123456} resp requests.post(url, jsonpayload) assert resp.status_code 200 data resp.json() assert data[code] 0 assert token in data[data]我自己的风格是状态码和业务code分开断言。HTTP 200只说明网络层面通了业务上可能还是返回一个“密码错误”的code。两者都断言才是完整的接口校验。接口测试还有一个关键点环境切换。开发环境、测试环境、预发环境base_url不一样。我会用一个fixture统一管理import pytest pytest.fixture def base_url(): return http://127.0.0.1:8000或者把环境配置写到环境变量里conftest.py统一读取。这样换环境跑测试时只需要改一个地方不用把几十个用例里的URL全改一遍。3.2 从单接口到流程用pytest串联多个接口单接口测试只能验证一个接口本身但很多业务问题出在接口之间的依赖上。比如登录拿到token然后用token去查用户信息这个过程就能串起来import pytest pytest.fixture def login_token(): resp requests.post(http://127.0.0.1:8000/api/login, json{username: admin, password: 123456}) data resp.json() assert data[code] 0 return data[data][token] def test_get_user_info(login_token): headers {Authorization: fBearer {login_token}} resp requests.get(http://127.0.0.1:8000/api/user/info, headersheaders) assert resp.status_code 200 user_data resp.json() assert user_data[data][username] admin这里fixture的返回值和自动清理机制就能复用起来了。如果需要这个token在每个测试用例前都重新获取就保持默认的function作用域如果整个测试过程只想登录一次可以加一个参数pytest.fixture(scopemodule) def login_token(): ...scope可选的还有class、session具体选择标准就一条看共享范围有多大。整个会话就登录一次的用session每个模块登录一次的用module。Fixtures设计好测试代码的重复度能降一大截。3.3 UI自动化入门Selenium的配置与核心点UI自动化是很多人入门自动化测试的第一个方向我自己也是从Selenium开始接触的。先安装pip install selenium然后去下载对应浏览器的driver。比如Chrome浏览器需要下载chromedriver版本必须和浏览器版本对应。这一步经常有人卡住版本对不上启动就会报SessionNotCreatedException。示例代码from selenium import webdriver from selenium.webdriver.common.by import By def test_search(): driver webdriver.Chrome() driver.get(https://example.com) search_input driver.find_element(By.ID, search) search_input.send_keys(python测试) search_input.submit() assert python测试 in driver.title driver.quit()我这里有个很重要的建议定位元素优先用ID、name这种稳定属性其次是CSS选择器最后才考虑XPath。XPath虽然灵活但一旦页面结构变化XPath基本必挂。另外一定要用显式等待别用time.sleep。Selenium自带的WebDriverWait可以轮询判断元素是否出现比死等几秒快得多也更不容易因为网络波动误报失败。UI测试因为运行慢、依赖环境我的用量策略是只写核心路径的用例比如登录、主流程下单、核心页面跳转。次要页面我基本不写UI用例转而用接口测试覆盖。3.4 数据驱动用数据文件减少重复代码自动化测试做多了会发现大量用例只是输入不同、断言值不同代码结构完全一样。这时候可以用数据驱动来管理。我常用的做法是把测试数据放到JSON文件里[ {input: {username: admin, password: 123456}, expected: 0}, {input: {username: admin, password: wrong}, expected: 10001} ]然后配合pytest的parametrize读取import json import pytest with open(data/login_cases.json, r, encodingutf-8) as f: login_cases json.load(f) pytest.mark.parametrize(case, login_cases) def test_login_param(case): resp requests.post(http://127.0.0.1:8000/api/login, jsoncase[input]) assert resp.json()[code] case[expected]数据从代码中分离出来之后后面新增用例只需要改JSON文件不需要动Python代码。测试数据本身也存在隔离的问题我一般不在生产环境跑用例测试库里的数据也要定期清理避免脏数据把断言结果带偏。4. 测试报告与持续集成让结果落地4.1 生成HTML测试报告pytest-html与Allure用例跑完了结果不能只在命令行里看。尤其你要给团队同步测试结果时一份看得过去的HTML报告比黑底白字的终端输出有说服力得多。安装pip install pytest-html运行pytest --htmlreport.html --self-contained-html打开report.html就能看到完整的用例通过情况、失败堆栈、运行时长样式还挺清爽。--self-contained-html这个参数很重要它的作用是把所有CSS、JS内联到单个HTML文件里方便直接发给别人或者作为附件存档。如果项目对报告要求更高比如想看趋势图、历史记录、按模块分组可以用Allure。操作略微复杂一点但报告颜值和维度确实好。基本流程pip install allure-pytest pytest --alluredirallure-results # 然后需要命令行工具 allure 才能生成网页报告 allure serve allure-resultsAllure的报告会汇总每个用例的附件、步骤、参数还能生成执行历史对比。不过我第一次用的时候也被折腾了一下allure命令行工具需要Java环境先装JDK才能运行。如果你只是为了自己看结果pytest-html就够了需要各种展示功能才上Allure。4.2 日志与失败现场排查问题的重要线索用例失败是常态关键是失败之后能不能快速知道为什么失败。我实践中遇到最多的场景是测试环境不稳定接口偶发超时用例挂了但重跑一遍又好了。如果没有日志这种偶发问题排查起来相当痛苦。我的做法是在conftest.py里统一配置loggingimport logging import pytest pytest.fixture(scopesession, autouseTrue) def setup_logging(): logging.basicConfig( levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(test.log, encodingutf-8) ] )然后在测试代码里需要的地方加logger.info记录关键步骤比如“请求发送成功响应耗时0.3秒”。一旦用例失败打开test.log能还原当时的现场。遇到偶发失败把对应的时间戳、响应内容、响应时长找出来基本就能判断是网络问题还是代码问题。UI测试里还有一个经典操作用例失败时自动截图。用pytest的钩子函数可以轻松实现这里给个思路Selenium的driver在测试结束时如果发现失败就把当前页面截图保存到指定目录。这个技巧在排查UI问题时价值极高没有截图光报一个“找不到按钮”的异常定位成本会高很多。4.3 定时跑测试把例行回归交给机器手动触发测试只是第一步。我自己的习惯是让测试定时跑不用天天惦记。最简单的方案是GitHub Actions或者GitLab CI也可以直接用操作系统的定时任务。我用GitHub Actions举个例子。在项目根目录建.github/workflows/test.ymlname: Python Tests on: push: schedule: # 每天凌晨2点跑一次 - cron: 0 2 * * * jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv5 with: python-version: 3.12 - name: Install dependencies run: | pip install -r requirements.txt pip install pytest pytest-html - name: Run tests run: pytest --htmlreport.html --self-contained-html - name: Upload report uses: actions/upload-artifactv4 with: name: test-report path: report.html配置好之后每次代码push或者每天凌晨2点测试都会自动跑一遍报告自动归档。第二天上班第一件事就是看通知绿了安心红了赶紧定位。我自己在实际项目中的体会是定时任务的稳定性比手动跑要高很多。因为定时跑是固定环境不会出现“哦我这个依赖忘了装”这种人为因素跑挂了大概率是代码真实有问题。5. 常见问题与排查那些年踩过的坑5.1 环境与安装问题速查表自动化测试跑了几年环境相关的问题永远是新手老大难。我把最常见的几类整理成了一张表方便对照排查问题现象根本原因解决办法命令行提示python不是内部或外部命令安装时没勾选Add to PATH重新安装Python并勾选或用完整路径启动vscode里运行提示找不到pytest解释器选成了全局环境CtrlShiftP选择正确的虚拟环境pip install速度极慢或超时默认镜像源在国外换国内镜像pip install -i https://pypi.tuna.tsinghua.edu.cn/simpleselenium报Driver版本错误chromedriver和浏览器版本不匹配下载和浏览器大版本一致的driver或用WebDriver Manager自动匹配中文输出乱码控制台编码与Python输出编码不一致Windows下执行chcp 65001切换UTF-8或日志文件指定encodingutf-85.2 断言与数据陷阱接口返回的时间戳、订单号这类动态数据直接断言“等于某个值”肯定挂。我的经验是动态字段断言类型和格式静态字段断言内容。时间戳断言它是个数字或者能被datetime解析订单号断言它的长度和前缀规则。这样用例既稳定又能覆盖核心逻辑。还有字典比较。直接用assert dict_a dict_b时两个字典里的字段顺序不同没关系Python会比对键值对内容。但如果字典里有嵌套结构出错信息会很长很难看。这时候可以用deepdiff之类的库它能明确告诉你哪一层的哪个字段不一致。遇到外部依赖要隔离时mock很有用。比如某个测试里依赖第三方短信接口这个接口既不稳定又要花钱测试时直接mock掉模拟返回指定结果用例就彻底可控了。说实话我刚入行时不会mock导致集成测试经常“看天吃饭”后来学会mock之后单元测试的稳定性直线上升。5.3 测试数据的隔离与清理自动化测试一个非常容易踩的坑是测试数据互相污染。举个例子注册用例跑一遍给数据库里写入一个固定手机号第二次再跑提示该手机号已经注册用例挂掉。我的习惯是每个用例用动态数据或者结束时清理数据。动态数据可以用时间戳来生成import time unique_num int(time.time()) mobile f138{unique_num % 100000000}但动态数据也有问题失败过后数据还在库里第三次续跑可能又冲突。这时候就需要在fixture的yield后面做数据清理。该删的记录要删该重置的状态要重置不能只创建不清理。还有一种常见问题是全局变量污染。我见过有人把测试结果存在全局list里用例A往里面写了数据用例B的断言读到了这个数据偶然地通过了。这是非常危险的设计因为它掩盖了用例之间真实的不独立性。pytest里每个测试函数默认是独立运行的fixture的function作用域也保证了每次重新创建。除非你有明确理由否则不要让用例之间共享可变全局状态。5.4 偶发失败与重试机制自动化测试的另一个痛点是偶发失败用例上一次全绿这次同一个用例挂了一只点开报错却是timeout。这类问题靠“硬件配置”还不够最开始可以先用pytest-flaky做重试pip install pytest-rerunfailures pytest --reruns 2 --reruns-delay 1这样偶发失败会自动重跑两次第二次过了就算过。但注意重试只能治标不能治本。如果某个用例经常需要重试才能过说明它本身不稳定要去找原因比如等待时间不足、外部接口响应慢、数据未清干净。我用重试的目的只是让CI流水线不被偶发问题卡住真正的问题还是要在报告和日志里暴露出来。6. 从我自己的经验出发最后说几点做Python测试这几年最大的体感是自动化测试的难点从来不是写用例而是写出稳定、可维护、有人愿意继续维护的用例。用例不稳定天天红团队慢慢就不看了这套自动化最终就废了。所以我后来给团队定的规矩是不追求用例数量多追求每个用例都稳定可靠。一个用例如果三天两头因为环境问题挂掉那就要么修好它要么删掉它绝对不允許靠重试掩盖问题。如果你现在刚开始接触Python测试我的建议很直接先把环境配好把pytest的fixture和参数化用熟然后从一个接口测试项目开始练手加上HTML报告定时跑起来。完成这个闭环之后你对自动化测试的理解一定会上一个台阶。别指望一口气把UI、接口、单元测试全铺开先从一个能落地的场景开始把它跑稳再往外扩展。这个路径我自己走过目前在团队里实施的效果也确实不错希望你也一样顺利。