做技术这些年,最让我头疼的不是代码写不出来,而是代码改完不知道会不会把老功能弄坏。尤其是刚接触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.28,B项目需要2.31,两个一冲突就互相打架。虚拟环境就是为了隔离这种依赖冲突。创建和激活的命令如下:
python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate激活后命令行前面会出现(venv)标记,这时候pip安装的所有包只会进当前项目。这步看起来多花了几秒钟,但后续所有测试依赖都能被锁在项目内部,单就这一点就少踩很多坑。
编辑器方面,vscode和pycharm都行。vscode装好Python扩展后,按Ctrl+Shift+P,输入“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.approx:
import pytest def test_float(): assert abs(0.1 + 0.2 - 0.3) < 1e-9 # 或者直接用: assert 0.1 + 0.2 == pytest.approx(0.3)还有异常断言。如果你测试一个函数是否正常抛异常,直接写pytest.raises:
def 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 --tb=short # 缩短错误堆栈,不刷屏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, json=payload) 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": f"Bearer {login_token}"} resp = requests.get("http://127.0.0.1:8000/api/user/info", headers=headers) assert resp.status_code == 200 user_data = resp.json() assert user_data["data"]["username"] == "admin"这里fixture的返回值和自动清理机制就能复用起来了。如果需要这个token在每个测试用例前都重新获取,就保持默认的function作用域;如果整个测试过程只想登录一次,可以加一个参数:
@pytest.fixture(scope="module") 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", encoding="utf-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", json=case["input"]) assert resp.json()["code"] == case["expected"]数据从代码中分离出来之后,后面新增用例只需要改JSON文件,不需要动Python代码。测试数据本身也存在隔离的问题:我一般不在生产环境跑用例,测试库里的数据也要定期清理,避免脏数据把断言结果带偏。
4. 测试报告与持续集成:让结果落地
4.1 生成HTML测试报告:pytest-html与Allure
用例跑完了,结果不能只在命令行里看。尤其你要给团队同步测试结果时,一份看得过去的HTML报告比黑底白字的终端输出有说服力得多。
安装:
pip install pytest-html运行:
pytest --html=report.html --self-contained-html打开report.html就能看到完整的用例通过情况、失败堆栈、运行时长,样式还挺清爽。--self-contained-html这个参数很重要,它的作用是把所有CSS、JS内联到单个HTML文件里,方便直接发给别人或者作为附件存档。
如果项目对报告要求更高,比如想看趋势图、历史记录、按模块分组,可以用Allure。操作略微复杂一点,但报告颜值和维度确实好。基本流程:
pip install allure-pytest pytest --alluredir=allure-results # 然后需要命令行工具 allure 才能生成网页报告 allure serve allure-resultsAllure的报告会汇总每个用例的附件、步骤、参数,还能生成执行历史对比。不过我第一次用的时候也被折腾了一下:allure命令行工具需要Java环境,先装JDK才能运行。如果你只是为了自己看结果,pytest-html就够了;需要各种展示功能才上Allure。
4.2 日志与失败现场:排查问题的重要线索
用例失败是常态,关键是失败之后能不能快速知道为什么失败。我实践中遇到最多的场景是:测试环境不稳定,接口偶发超时,用例挂了,但重跑一遍又好了。如果没有日志,这种偶发问题排查起来相当痛苦。
我的做法是在conftest.py里统一配置logging:
import logging import pytest @pytest.fixture(scope="session", autouse=True) def setup_logging(): logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[ logging.FileHandler("test.log", encoding="utf-8") ] )然后在测试代码里需要的地方加logger.info记录关键步骤,比如“请求发送成功,响应耗时0.3秒”。一旦用例失败,打开test.log能还原当时的现场。遇到偶发失败,把对应的时间戳、响应内容、响应时长找出来,基本就能判断是网络问题还是代码问题。
UI测试里还有一个经典操作:用例失败时自动截图。用pytest的钩子函数可以轻松实现,这里给个思路,Selenium的driver在测试结束时如果发现失败,就把当前页面截图保存到指定目录。这个技巧在排查UI问题时价值极高,没有截图,光报一个“找不到按钮”的异常,定位成本会高很多。
4.3 定时跑测试:把例行回归交给机器
手动触发测试只是第一步。我自己的习惯是让测试定时跑,不用天天惦记。最简单的方案是GitHub Actions或者GitLab CI,也可以直接用操作系统的定时任务。
我用GitHub Actions举个例子。在项目根目录建.github/workflows/test.yml:
name: Python Tests on: push: schedule: # 每天凌晨2点跑一次 - cron: "0 2 * * *" jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install dependencies run: | pip install -r requirements.txt pip install pytest pytest-html - name: Run tests run: pytest --html=report.html --self-contained-html - name: Upload report uses: actions/upload-artifact@v4 with: name: test-report path: report.html配置好之后,每次代码push或者每天凌晨2点,测试都会自动跑一遍,报告自动归档。第二天上班第一件事就是看通知:绿了,安心;红了,赶紧定位。
我自己在实际项目中的体会是:定时任务的稳定性比手动跑要高很多。因为定时跑是固定环境,不会出现“哦我这个依赖忘了装”这种人为因素,跑挂了大概率是代码真实有问题。
5. 常见问题与排查:那些年踩过的坑
5.1 环境与安装问题速查表
自动化测试跑了几年,环境相关的问题永远是新手老大难。我把最常见的几类整理成了一张表,方便对照排查:
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 命令行提示python不是内部或外部命令 | 安装时没勾选Add to PATH | 重新安装Python并勾选,或用完整路径启动 |
| vscode里运行提示找不到pytest | 解释器选成了全局环境 | Ctrl+Shift+P选择正确的虚拟环境 |
| pip install速度极慢或超时 | 默认镜像源在国外 | 换国内镜像:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple |
| selenium报Driver版本错误 | chromedriver和浏览器版本不匹配 | 下载和浏览器大版本一致的driver,或用WebDriver Manager自动匹配 |
| 中文输出乱码 | 控制台编码与Python输出编码不一致 | Windows下执行chcp 65001切换UTF-8,或日志文件指定encoding="utf-8" |
5.2 断言与数据陷阱
接口返回的时间戳、订单号这类动态数据,直接断言“等于某个值”肯定挂。我的经验是:动态字段断言类型和格式,静态字段断言内容。时间戳断言它是个数字或者能被datetime解析;订单号断言它的长度和前缀规则。这样用例既稳定又能覆盖核心逻辑。
还有字典比较。直接用assert dict_a == dict_b时,两个字典里的字段顺序不同没关系,Python会比对键值对内容。但如果字典里有嵌套结构,出错信息会很长很难看。这时候可以用deepdiff之类的库,它能明确告诉你哪一层的哪个字段不一致。
遇到外部依赖要隔离时,mock很有用。比如某个测试里依赖第三方短信接口,这个接口既不稳定又要花钱,测试时直接mock掉,模拟返回指定结果,用例就彻底可控了。说实话我刚入行时不会mock,导致集成测试经常“看天吃饭”,后来学会mock之后,单元测试的稳定性直线上升。
5.3 测试数据的隔离与清理
自动化测试一个非常容易踩的坑是测试数据互相污染。举个例子:注册用例跑一遍,给数据库里写入一个固定手机号;第二次再跑,提示该手机号已经注册,用例挂掉。
我的习惯是:每个用例用动态数据,或者结束时清理数据。动态数据可以用时间戳来生成:
import time unique_num = int(time.time()) mobile = f"138{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、接口、单元测试全铺开,先从一个能落地的场景开始,把它跑稳,再往外扩展。这个路径,我自己走过,目前在团队里实施的效果也确实不错,希望你也一样顺利。