PandHedge

PyTest基础

2025-09-13
PandHedge

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
五、参数化测试的最佳实践
  1. 保持测试原子性 每个参数化测试应只验证一个逻辑,避免测试用例过于复杂。

  2. 使用描述性参数名 参数名应清晰表达测试意图,例如

    @pytest.mark.parametrize(
        "username, password, expected_status",
        [
            ("valid_user", "correct_pass", 200),
            ("invalid_user", "wrong_pass", 401),
        ]
    )
    
  3. 避免参数过多 过多参数会使测试难以理解,建议每组参数不超过 5-6 个。

  4. 数据驱动测试

    将测试数据与测试逻辑分离(如使用 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) 是测试代码的核心机制,用于验证程序的实际行为是否符合预期。以下是关于断言的作用、处理断言成功 / 失败的详细说明:

断言的作用

  1. 验证功能正确性 通过断言检查代码的输出、状态或行为是否符合预期,确保功能正常工作。

    def test_addition():
        assert 2 + 2 == 4  # 验证加法功能
    
  2. 自动化测试 断言使测试可以自动运行并判断结果,无需人工干预。

  3. 错误定位 当断言失败时,pytest 会提供详细的错误信息(如实际值与期望值),帮助快速定位问题。

  4. 文档化预期行为 断言代码本身就是对功能预期行为的明确文档,便于团队理解和维护。

处理断言成功

1. 使用 assert 无异常

如果断言成功,代码会继续执行后续逻辑。

2. 使用 pytest.mark.skipxfail

若某些条件下测试不需要执行,可以标记测试为跳过或预期失败。

处理断言失败

当断言失败时,pytest 会显示详细的错误信息,包括实际值和期望值。

1. 自定义错误信息

assert 添加详细的失败提示,帮助理解问题。

2. 使用 pytest.raises 捕获异常

验证代码是否按预期抛出异常。

3. 使用 try-except 手动处理 使用 pytest.fail 主动失败

在特殊场景下,可以用 try-except 捕获异常并自定义处理逻辑(但这种方式在测试中较少使用)。

最佳实践

  1. 每个测试专注一个断言 保持测试用例的原子性,避免一个测试包含多个不相关的断言。
  2. 使用描述性的断言信息 让失败信息清晰易懂,减少调试成本。
  3. 结合 pytest.mark 标记特殊测试@pytest.mark.skip@pytest.mark.xfail 等。
  4. 避免过度断言 只验证必要的结果,不要断言实现细节。

第三方插件

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

五、注意事项

  1. 测试隔离:确保每个测试用例相互独立,避免状态污染。
  2. 性能考虑:频繁的真实网络请求会降低测试速度,优先使用本地服务器或模拟。
  3. 错误处理:测试中处理网络异常(如超时、连接错误)。

六、常用插件

  • 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)
其他注意事项
  1. 浏览器对 GET 的处理

    • 浏览器地址栏输入 URL 并访问时,默认使用 GET。
    • <form> 表单的 method 属性默认为 GET(需显式设置为 POST)。
  2. 性能差异

    • GET 通常比 POST 稍快(请求头更简单,无需处理请求体)。
    • 但对于复杂操作(如大数据传输),POST 的优势更明显。
  3. RESTful API 规范

    • GET:用于读取资源(不修改数据)。
    • POST:用于创建资源。
    • PUT/PATCH:用于更新资源。
    • DELETE:用于删除资源。
  4. 幂等(Idempotent):多次执行同一操作产生的效果与一次执行相同。

    非幂等:多次执行会产生不同效果。

常见场景

  1. 表单参数

主要用在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"
#      }
    )
  1. JSON参数

    主要用在各类项目:

    1.参数类型很多

    2,请求头中包含json

requests技巧:

如果传递json参数,则自动将其识别为json,并添加请求头

def test_api_form():
    
    resp = resquests.request(
    	method= 'post',
    	url='url',
        json=user_info, #json方式传参

    )
  1. 文件上传

主要用在各类项目:

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}, #表单上传文件的方式传参
    )
  1. 接口关联

    依赖 关联

编程语言基础:

  1. 创建变量

  2. 传递变量
  3. 获取变量
  4. 打印变量

如何从上一个响应中提取变量:

  • 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


  1. 参数化测试

数据驱动测试 = 参数化测试+数据文件

#####

请求头(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/json
  • Accept-Language 客户端期望的响应语言(如中文、英文)。 示例:Accept-Language: zh-CN,en-US
  • Cache-Control 控制缓存策略(如 no-cachemax-age=3600)。 示例:Cache-Control: no-cache

2. 请求头(Request Headers)

  • Host 请求的目标服务器域名和端口。 示例:Host: api.example.com
  • Authorization 身份验证信息(如令牌、用户名密码)。 示例:Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
  • Cookie 客户端存储的 Cookie 信息,用于会话跟踪。 示例:Cookie: session_id=12345; user=test
  • Referer 表示请求的来源页面 URL(注意拼写,没有 r)。 示例:Referer: https://example.com/search
  • Origin 表示请求的来源域名(不含路径),主要用于 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
  • Content-Length 请求体的字节长度。 示例:Content-Length: 128
  • Content-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 资源未被修改,可以使用缓存版本(通常配合 ETagLast-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 服务器作为网关或代理,未及时从上游服务器获取响应。

##


上一篇 NumPy基础

下一篇 Pyhton基础

Comments

Content