PyTest 快速掌握
PyTest 快速掌握
pytest **是组织和运行测试的框架,提供断言、fixture、参数化等功能。
【框架】
【测试框架】
- 用例发现
- 用例筛选
- 用例执行
- 用例报告
- 第三方插件
安装
pip install pytest
创建用例
def test_abd()
assert 1==2
要求:
- 创建以test_开头的py文件
- 创建以test_开头的函数
- 在函数中使用断言
执行用例
pytest #执行用例
pytest -h #查询参数、配置、使用说明
参数化测试
在 pytest 中,参数化测试是一种强大的特性,允许你使用不同的参数多次运行同一个测试函数,从而覆盖更多的测试场景。
一、@pytest.mark.parametrize 装饰器
这是 pytest 最常用的参数化方式,通过装饰器为测试函数提供多组参数。
1. 基本用法(单参数)
import pytest
@pytest.mark.parametrize("input_value", [1, 2, 3])
def test_double(input_value):
assert input_value * 2 == input_value + input_value
# 等价于运行三次测试:
# test_double(1)
# test_double(2)
# test_double(3)
2. 多参数组合
@pytest.mark.parametrize(
"a, b, expected", # 参数名,用逗号分隔
[
(1, 2, 3), # 第一组参数:a=1, b=2, expected=3
(0, 0, 0), # 第二组参数
(-1, 1, 0), # 第三组参数
]
)
def test_add(a, b, expected):
assert a + b == expected
3. 参数化与 fixtures 结合
import pytest
@pytest.fixture
def setup_database():
# 初始化数据库连接
db = connect_to_db()
yield db # 返回数据库对象
db.close() # 清理资源
@pytest.mark.parametrize("user_id", [1, 2, 3])
def test_user_data(setup_database, user_id):
data = setup_database.get_user(user_id)
assert data is not None
二、动态生成参数
使用 pytest_generate_tests 钩子函数动态生成测试参数:
# conftest.py
def pytest_generate_tests(metafunc):
if "input_data" in metafunc.fixturenames:
# 从文件或 API 获取测试数据
metafunc.parametrize("input_data", get_test_data_from_db())
# test_example.py
def test_with_dynamic_data(input_data):
assert process_data(input_data) == expected_output
三、参数化测试的高级用法
1. 标记特定参数组合
使用 pytest.param 为特定参数组合添加标记(如跳过、预期失败):
@pytest.mark.parametrize(
"dividend, divisor, expected",
[
(10, 2, 5),
pytest.param(10, 0, None, marks=pytest.mark.xfail), # 预期失败
pytest.param(10, -2, -5, marks=pytest.mark.skipif(condition)), # 跳过
]
)
def test_divide(dividend, divisor, expected):
if divisor == 0:
with pytest.raises(ZeroDivisionError):
dividend / divisor
else:
assert dividend / divisor == expected
2. 多参数化装饰器组合
多个 @pytest.mark.parametrize 会生成参数的笛卡尔积:
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [3, 4])
def test_product(x, y):
assert x * y < 10 # 测试 1*3, 1*4, 2*3, 2*4
四、命令行参数
通过 pytest_addoption 钩子函数添加自定义命令行参数:
# conftest.py
def pytest_addoption(parser):
parser.addoption("--env", action="store", default="dev", help="Environment: dev, test, prod")
@pytest.fixture
def environment(request):
return request.config.getoption("--env")
# test_example.py
def test_environment(environment):
if environment == "prod":
pytest.skip("跳过生产环境测试")
# 执行测试...
运行时指定参数:
pytest --env=test
五、参数化测试的最佳实践
-
保持测试原子性 每个参数化测试应只验证一个逻辑,避免测试用例过于复杂。
-
使用描述性参数名 参数名应清晰表达测试意图,例如
@pytest.mark.parametrize( "username, password, expected_status", [ ("valid_user", "correct_pass", 200), ("invalid_user", "wrong_pass", 401), ] ) -
避免参数过多 过多参数会使测试难以理解,建议每组参数不超过 5-6 个。
-
数据驱动测试
将测试数据与测试逻辑分离(如使用 JSON/CSV 文件存储参数):
import json def load_test_data(): with open("test_data.json") as f: return json.load(f) @pytest.mark.parametrize("data", load_test_data()) def test_with_external_data(data): assert process(data) == expected
六、相关插件
- pytest-cases:更强大的参数化和测试用例组织。
- pytest-xdist:并行运行参数化测试,提高效率。
- pytest-bdd:结合 Gherkin 语法实现行为驱动的参数化测试。
assert 断言
assert result == 5, f"计算错误:预期5,实际{result}" # 条件成立,无反应
assert 是许多编程语言(如 Python、Java、C++ 等)都支持的关键字或函数,其核心逻辑是:
“如果某个条件为真,则继续执行;如果为假,则抛出错误并终止程序(或测试)”。
它的本质是一种 “自我检查”,用于确认代码运行过程中某些 “必须成立” 的前提条件,例如:
- 函数参数的合法性(如不能为
None) - 计算结果的合理性(如数值范围、数据格式)
- 程序执行到某一步时的状态(如列表不为空)
断言常见用法:
# 测试加法功能
assert 1 + 1 == 2 # 成功:测试通过
assert 2 + 3 == 6 # 失败:测试标记为FAILED,输出错误信息
def test_api():
response = requests.get("https://api.example.com/user")
# 断言状态码为200(成功)
assert response.status_code == 200, "API请求失败"
# 断言返回数据为JSON格式,且包含"name"字段
data = response.json()
assert "name" in data, "响应数据缺少name字段"
def process_data(data):
# 确保输入数据是列表类型
assert isinstance(data, list), "data必须是列表"
# 确保列表不为空
assert len(data) > 0, "data不能为空列表"
# 处理数据...
assert a == pytest.approx(0.3) # 断言近似相等
assert hasattr(p, "name") # 断言对象有某个属性
assert p.age == 30 # 断言属性值
assert x < y and x > 0 # 组合条件
assert x in [1, 3, 5] or y == 10 # 或条件
不能替代正常的错误处理
assert 主要用于调试,在生产环境中可能被禁用(如 Python 运行时添加 -O 选项会忽略所有 assert)。
对于用户输入验证、可能出现的运行时错误(如文件不存在),应使用 if 判断 + 主动抛异常(如 ValueError)
在 pytest 中,断言(Assertion) 是测试代码的核心机制,用于验证程序的实际行为是否符合预期。以下是关于断言的作用、处理断言成功 / 失败的详细说明:
断言的作用
-
验证功能正确性 通过断言检查代码的输出、状态或行为是否符合预期,确保功能正常工作。
def test_addition(): assert 2 + 2 == 4 # 验证加法功能 -
自动化测试 断言使测试可以自动运行并判断结果,无需人工干预。
-
错误定位 当断言失败时,pytest 会提供详细的错误信息(如实际值与期望值),帮助快速定位问题。
-
文档化预期行为 断言代码本身就是对功能预期行为的明确文档,便于团队理解和维护。
处理断言成功
1. 使用 assert 无异常
如果断言成功,代码会继续执行后续逻辑。
2. 使用 pytest.mark.skip 或 xfail
若某些条件下测试不需要执行,可以标记测试为跳过或预期失败。
处理断言失败
当断言失败时,pytest 会显示详细的错误信息,包括实际值和期望值。
1. 自定义错误信息
为 assert 添加详细的失败提示,帮助理解问题。
2. 使用 pytest.raises 捕获异常
验证代码是否按预期抛出异常。
3. 使用 try-except 手动处理 使用 pytest.fail 主动失败
在特殊场景下,可以用 try-except 捕获异常并自定义处理逻辑(但这种方式在测试中较少使用)。
最佳实践
- 每个测试专注一个断言 保持测试用例的原子性,避免一个测试包含多个不相关的断言。
- 使用描述性的断言信息 让失败信息清晰易懂,减少调试成本。
- 结合
pytest.mark标记特殊测试 如@pytest.mark.skip、@pytest.mark.xfail等。 - 避免过度断言 只验证必要的结果,不要断言实现细节。
第三方插件
1. 使用测试服务器
- 本地服务器:测试时启动真实服务器(如 Flask 的
app.run())。 - 测试客户端:使用框架提供的测试客户端(如 Flask 的
app.test_client()),避免网络开销。
2. 模拟请求(Mock)
当测试外部 API 时,为避免依赖真实网络,可以使用 pytest-mock 模拟 requests:
# test_external_api.py
from unittest.mock import MagicMock
def test_external_api(mocker):
# 模拟 requests.get 方法
mock_get = mocker.patch("requests.get")
mock_get.return_value.json.return_value = {"key": "value"}
mock_get.return_value.status_code = 200
# 测试代码
response = requests.get("https://external.api/data")
assert response.json() == {"key": "value"}
3. 使用 Fixture 管理会话
# conftest.py (pytest 自动加载)
import pytest
import requests
@pytest.fixture
def session():
with requests.Session() as s:
# 可设置通用配置(如认证)
s.headers.update({"Authorization": "Bearer token"})
yield s
# test_with_fixture.py
def test_api_with_session(session):
response = session.get("http://localhost:5000/protected")
assert response.status_code == 200
五、注意事项
- 测试隔离:确保每个测试用例相互独立,避免状态污染。
- 性能考虑:频繁的真实网络请求会降低测试速度,优先使用本地服务器或模拟。
- 错误处理:测试中处理网络异常(如超时、连接错误)。
六、常用插件
- pytest-responses:用于模拟
requests调用,无需真实网络。 - pytest-xdist:并行运行测试,提高效率。
- requests-mock:专门用于模拟
requests的库。
掌握以下内容:
- 参数 【-s 关闭IO捕获】
- mark 标记、筛选用例
- fixture
- hook 【天花板】
@python.mark.skip 跳过
—
Requests 快速掌握
requests 是 Python 中最常用的 HTTP 客户端库,用于发送各种 HTTP 请求(如 GET、POST 等),获取网页内容或与 API 交互。
能够把requests 与 pytest 结合起来就能做web测试
安装与使用
pip install requests
向接口发送请求
(1) 发送 GET 请求
导入
import requests
def test_name():
resp = requests.request( #基本用法.变量resp接受返回,requests.request()发起请求
method='get', #HTTP请求方法,必填
url='https://www.baidu.com' #接口地址,必填
data={"a":1,} #接口参数,选填,类型有多种
)
print(response.text) # 获取响应文本 字符串
print(response.headers) #字典
print(response.json()) # 解析 JSON 响应 JSON
print(response.status_code) # 获取状态码 (200 表示成功) 整数
(2) 带参数的 GET 请求
- 场景:搜索、查询数据(如商品列表、用户信息)。
- 特点:请求可被缓存,参数可见,适合幂等操作。
params = {"key1": "value1", "key2": "value2"}
response = requests.get("https://api.example.com/search", params=params)
# 实际请求 URL: https://api.example.com/search?kekey1=value1&key2=value2
(3) 发送 POST 请求
- 场景:提交表单、创建资源(如注册用户、上传文件)。
- 特点:参数不可见,适合非幂等操作。
data = {"username": "user", "password": "pass"}
response = requests.post("https://api.example.com/login", data=data)
其他注意事项
-
浏览器对 GET 的处理
- 浏览器地址栏输入 URL 并访问时,默认使用 GET。
<form>表单的method属性默认为 GET(需显式设置为 POST)。
-
性能差异
- GET 通常比 POST 稍快(请求头更简单,无需处理请求体)。
- 但对于复杂操作(如大数据传输),POST 的优势更明显。
-
RESTful API 规范
- GET:用于读取资源(不修改数据)。
- POST:用于创建资源。
- PUT/PATCH:用于更新资源。
- DELETE:用于删除资源。
-
幂等(Idempotent):多次执行同一操作产生的效果与一次执行相同。
非幂等:多次执行会产生不同效果。
常见场景
-
表单参数
主要用在Web项目:
1.参数只能是字符串
2,请求头中包含form
requests技巧:
- 如果data数据是一个字典,则自动将其识别为表单,并添加请求头
uer_info = {
"username":"sanmu",
"password":"123456"
}
def test_api_form():
resp = resquests.request(
method= 'post',
url='url',
data=user_info, #表单方式传参
# headers={
# "content-type":"application/x-www-form-urlencoded"
# }
)
-
JSON参数
主要用在各类项目:
1.参数类型很多
2,请求头中包含json
requests技巧:
如果传递json参数,则自动将其识别为json,并添加请求头
def test_api_form():
resp = resquests.request(
method= 'post',
url='url',
json=user_info, #json方式传参
)
-
文件上传
主要用在各类项目:
1.上传方式:
- body直传
- 表单
2,请求头说明使用哪种方式
requests技巧:
- 如果传递file参数,自动识别为表单文件上传,自动添加请求头
def test_file_upload():
path = r"fileAdress" #文件地址,字符串
f = open(path,'r') #文件对象,打开文件
resp = resquests.request(
method= 'post',
url='apiUrl',
file={"fileName":f}, #表单上传文件的方式传参
)
-
接口关联
依赖 关联
编程语言基础:
-
创建变量
- 传递变量
- 获取变量
- 打印变量
如何从上一个响应中提取变量:
- re
- jsonapth : json接口
- xpath
流程:
1.请求第一个接口,得到响应
2.从响应中提取数据,创建变量
3.在第二个接口中使用变量
def test_token ():
resp = requests.request(
method='post',
url='http://api.fbi.com:9225/rest-v1/login/with_form',
data=user_info
)
assert resp.status_code == 200
print(resp.text)#返回token身份凭据
#变量提取
#变量连接上下两个接口
token = jsonpath.jsonpath(resp.json(),expr:'$.token')[0]
resp = requests.request(
method='get',
url='http://api.fbi.com:9225/rest-v1/auth/token_with_header',
headers={"token":token}
#没有提供身份凭据
)
assert resp.status_code == 200
-
参数化测试
数据驱动测试 = 参数化测试+数据文件
#####
请求头(Headers)
设置请求头模拟浏览器行为或传递认证信息:
headers = {
"User-Agent": "Mozilla/5.0",
"Authorization": "Bearer your_token_here"
}
response = requests.get("https://api.example.com/protected", headers=headers
在 HTTP 协议中,请求头(Request Headers) 是客户端(如浏览器、Python 的 requests 库)发送给服务器的额外信息,用于描述请求的元数据(如身份验证、内容类型、缓存策略等)。
常见请求头分类及示例
1. 通用头(General Headers)
User-Agent客户端的标识信息(如浏览器类型、操作系统)。 示例:User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)Accept客户端期望的响应内容类型(如 JSON、XML)。 示例:Accept: application/jsonAccept-Language客户端期望的响应语言(如中文、英文)。 示例:Accept-Language: zh-CN,en-USCache-Control控制缓存策略(如no-cache、max-age=3600)。 示例:Cache-Control: no-cache
2. 请求头(Request Headers)
Host请求的目标服务器域名和端口。 示例:Host: api.example.comAuthorization身份验证信息(如令牌、用户名密码)。 示例:Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...Cookie客户端存储的 Cookie 信息,用于会话跟踪。 示例:Cookie: session_id=12345; user=testReferer表示请求的来源页面 URL(注意拼写,没有r)。 示例:Referer: https://example.com/searchOrigin表示请求的来源域名(不含路径),主要用于 CORS。 示例:Origin: https://client.example.com
3. 实体头(Entity Headers)
Content-Type请求体的格式(如 JSON、表单数据)。 示例:- JSON:
Content-Type: application/json - 表单:
Content-Type: application/x-www-form-urlencoded - 文件上传:
Content-Type: multipart/form-data
- JSON:
Content-Length请求体的字节长度。 示例:Content-Length: 128Content-Encoding请求体的编码方式(如gzip)。 示例:Content-Encoding: gzip
处理响应
(1) 响应状态
if response.status_code == 200:
print("请求成功")
elif response.status_code == 404:
print("资源不存在")
else:
print(f"错误: {response.status_code}")
# 更简洁的写法(状态码非 200 时抛出异常)
response.raise_for_status()
(2) 响应内容
print(response.text) # 文本形式
print(response.content) # 二进制形式(用于下载文件)
print(response.json()) # JSON 解析(自动转换为字典)
(3) 获取响应头
print(response.headers) # 所有响应头
print(response.headers["Content-Type"]) # 获
高级用法
会话(Session)
保持同一会话的 cookies 和连接,适合需要登录的场景:
with requests.Session() as session:
# 登录请求(会保存 cookies)
session.post("https://example.com/login", data={"user": "me"})
# 使用同一会话发送后续请求
response = session.get("https://example.com/dashboard")
超时设置
避免请求无限等待:
response = requests.get("https://example.com", timeout=5) # 5 秒超时
处理重定向
response = requests.get("http://github.com", allow_redirects=True) # 默认 True
文件上传
files = {"file": open("data.csv", "rb")}
response = requests.post("https://example.com/upload", files=files)
异常处理
try:
response = requests.get("https://example.com", timeout=3)
response.raise_for_status() # 非 200 状态码时抛出异常
except requests.exceptions.Timeout:
print("请求超时")
except requests.exceptions.HTTPError as e:
print(f"HTTP 错误: {e}")
except requests.exceptions.RequestException as e:
print(f"其他错误: {e}")
认证
基本认证
from requests.auth import HTTPBasicAuth
response = requests.get(
"https://api.example.com/secure",
auth=HTTPBasicAuth("username", "password")
)
# 简写: auth=("username", "password")
OAuth 认证
headers = {"Authorization": "Bearer your_oauth_token"}
response = requests.get("https://api.example.com/oauth", headers=headers)
代理设置
proxies = {
"http": "http://proxy.example.com:8080",
"https": "http://proxy.example.com:8080"
}
response = requests.get("https://example.com", proxies=proxies)
常见场景示例
下载文件
url = "https://example.com/file.zip"
response = requests.get(url, stream=True) # stream=True 避免内存溢出
with open("file.zip", "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
API 请求示例
# 请求 GitHub API 获取用户信息
response = requests.get("https://api.github.com/users/octocat")
data = response.json()
print(data["name"]) # 输出: The Octocat
日志、报告、插件等框架封装
pytest是一个专注用例执行的框架,不只是单元测试
如果需要增加新的功能,需要借助第三方插件
I-P-O
输入简单:用例不上代码,只是数据 .ymak
输出简单:不上文本 allure 测试报告
处理灵活:记录日志、发送通知
常见状态码:
状态码的第一个数字定义了响应的类别,后两位无分类作用:
- 1xx(信息性状态码):表示临时响应,需要客户端继续操作。
- 2xx(成功状态码):表示请求已成功被服务器接收、理解并处理。
- 3xx(重定向状态码):表示需要客户端进一步操作(如重定向)。
- 4xx(客户端错误状态码):表示客户端可能存在错误,请求无法被服务器处理。
- 5xx(服务器错误状态码):表示服务器在处理请求时发生了错误。
常见状态码详解
1. 2xx(成功)
- 200 OK 最常见的成功状态码,表示请求已成功处理。
- 201 Created 表示请求成功并创建了新资源(如 POST 请求创建用户后返回)。
- 204 No Content 表示请求成功,但响应中没有实体内容(如 DELETE 请求成功后返回)。
2. 3xx(重定向)
- 301 Moved Permanently 永久重定向,资源已永久移动到新 URL,浏览器会缓存该重定向。
- 302 Found 临时重定向,资源临时移动到新 URL,客户端应继续使用原 URL。
- 304 Not Modified
资源未被修改,可以使用缓存版本(通常配合
ETag或Last-Modified头使用)。 - 307 Temporary Redirect 临时重定向,与 302 类似,但客户端应保持原请求方法(如 POST 不会变为 GET)。
3. 4xx(客户端错误)
- 400 Bad Request 客户端请求有语法错误,不能被服务器所识别(如参数格式错误)。
- 401 Unauthorized 请求需要身份验证(如未登录访问需要权限的资源)。
- 403 Forbidden 服务器理解请求客户端的请求,但拒绝执行此请求(如权限不足)。
- 404 Not Found 请求的资源不存在(如 URL 拼写错误)。
- 405 Method Not Allowed 请求方法(如 GET、POST)不被允许(如 API 只支持 POST,但客户端发送了 GET)。
- 408 Request Timeout 客户端请求超时,服务器等待时间过长。
- 429 Too Many Requests 客户端发送请求过多,超出服务器限制(常见于限流场景)。
4. 5xx(服务器错误)
- 500 Internal Server Error 服务器内部错误,无法完成请求(最常见的服务器端错误)。
- 502 Bad Gateway 服务器作为网关或代理,从上游服务器收到无效响应。
- 503 Service Unavailable 服务器暂时过载或维护,无法处理请求。
- 504 Gateway Timeout 服务器作为网关或代理,未及时从上游服务器获取响应。
##