PandHedge

pytest

2025-09-18
PandHedge

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 服务器作为网关或代理,未及时从上游服务器获取响应。

Pytest

接口自动化:Pytest测试框架(单元测试)与PO设计模式

1.1概念

pytest 是 python的一种单元测试框架,同自带的UnitTest测试框架类似,相比于UnitTest框架使用起来更简洁,效率更高。

pytest 测试框架

定义:测试框架是一套标准化的工具、规则和结构的集合,为测试活动提供基础支撑,目的是简化测试过程、提高效率、确保测试的规范性和可维护性。

  • 测试框架是 “舞台”,提供表演的规则和设施;
  • 脚本编写是 “剧本创作”,根据规则写出具体的测试内容;
  • 执行测试是 “演戏”,按照剧本在舞台上实际运行并呈现结果。
1.2特点

1.非常容易上手,入门简单,文档丰富,文档中有很多实例可以参考 2.支持简单的单元测试和复杂的功能测试 3.支持参数化(utitest默认没有参数化) 4.执行测试过程中可以将某些测试跳过,或者对某些预期失败的Case标记成失败 5.支持重复执行失败的Case 6.支持运行由Nose,UnitTest 编写的测试Case(pytest兼容utitest) 7.具有很多第三方插件,并且可以自定义扩展

Pytest Plugin List - pytest documentation

8.方便的和持续集成工具集成

持续集成:当代码发生变化时,集成到共享仓库,并借助自动化工具完成构建、测试等流程,从而尽早发现和解决代码集成中的问题,确保团队开发的代码始终保持可工作状态。

2.创建与执行

创建

函数形式:

函数名

def test_fun():

image-20250917191034646

类的形式

class TestName(object):

image-20250917191600512

执行

命令行执行:

pytest -s file.py

主函数形式运行:

if __name__== __main__
#语法:pytest.main(['-s','文件名.py'])
pytest.main(['-s','hmo3_pytest_basic_03.py'])

image-20250917191901157

特殊方法:

特殊方法:函数级别

开始函数——》测试函数111——》结束函数——》开始函数——》测试函数222——》结束函数

# 特殊方法:函数级别
import pytest

class TestDemo(object):
#说明:特殊方法名写法固定,没有代码提示,需要手写!
#注意:函数级别执行顺序:先setup()方法->测试方法->teardown()方法

    def setup_method(self):
    # 开始方法
        print('函数->开始')
        
    def teardown_method(self):
    # 结束方法
        print('函数->结束')
    
    def test_method1(self):
    # ”示例测试方法1
        print('测试方法1')
              
              
    def test_method2(self):
    # 示例测试方法2
        print('测试方法2')
        
if __name__ == '__main__':
     pytest.main(['-s','test.py'])

输出:

开始函数——》测试函数111——》结束函数——》开始函数——》测试函数222——》结束函数

PS C:\Users\lin\Downloads\Project\lin\python\study\test> py test.py
============================== test session starts ===============================        
platform win32 -- Python 3.10.10, pytest-8.4.1, pluggy-1.6.0
rootdir: C:\Users\lin\Downloads\Project\lin\python\study\test
plugins: anyio-3.7.1, asyncio-1.1.0
asyncio: mode=strict, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 2 items

test.py 函数->开始
测试方法1
.函数->结束
函数->开始
测试方法2
.函数->结束

特殊方法:类级别

类开始函数——》测试函数111——》测试函数222——》类结束函数

class TestLogin:
# 测试类级开始
def setup_class(self):
print("------->setup_class")
# 特殊方法:类级别
import pytest

class TestDemo(object):
#说明:特殊方法名写法固定,没有代码提示,需要手写!
#注意:函数级别执行顺序:先setup()方法->测试方法->teardown()方法

    def setup_class(self):
    # 开始方法
        print('函数->开始')
        
    def teardown_class(self):
    # 结束方法
        print('函数->结束')
    
    def test_method1(self):
    # ”示例测试方法1
        print('测试方法1')
              
              
    def test_method2(self):
    # 示例测试方法2
        print('测试方法2')
        
if __name__ == '__main__':
     pytest.main(['-s','test.py'])

输出:

collected 2 items                                                                         

test.py 函数->开始
测试方法1
.测试方法2
.函数->结束

混合使用

# 特殊方法:混合
import pytest

class TestDemo(object):
#说明:特殊方法名写法固定,没有代码提示,需要手写!
#注意:函数级别执行顺序:先setup()方法->测试方法->teardown()方法

    def setup_class(self):
    # 开始方法
        print('#1 类函数->开始')
        
    def teardown_class(self):
    # 结束方法
        print('#2 类函数->结束\n' )
        
    def setup_method(self):
    # ”示例测试方法1
        print('#3 函数->开始')
              
              
    def teardown_method(self):
    # 示例测试方法2
        print('#4 函数->结束\n')
    
    def test_method1(self):
    # ”示例测试方法1
        print('#5 测试方法1')
              
              
    def test_method2(self):
    # 示例测试方法2
        print('#6 测试方法2')
        
if __name__ == '__main__':
     pytest.main(['-s','test.py'])

输出:

collected 2 items

test.py #1 类函数->开始
#3 函数->开始
#5 测试方法1
.#4 函数->结束

#3 函数->开始
#6 测试方法2
.#4 函数->结束

#2 类函数->结束

3.配置文件

应用场景 使用配置文件后可以快速的使用配置的项来选择执行哪些测试模块。 使用方式 1.项目下新建scripts模块 2.将测试脚本文件放到scripts中 3.pytest的配置文件放在自动化项目目录下 4.名称为 pytest.ini 5.命令行运行时会使用该配置文件中的配置 6.第一行内容为[pytest]

pytest.ini 是 pytest 框架的核心配置文件,用于自定义测试运行的行为(如指定测试路径、设置默认参数、配置插件等)。pytest 运行时会自动读取项目根目录下的 pytest.ini 文件,根据配置覆盖默认行为,无需每次通过命令行传递大量参数。

基本作用

  • 简化命令行操作:将常用命令行参数(如 -s-v)设置为默认值。

    其中:

    -s:显示测试中 print() 的输出 -v:显示详细的测试结果(包含每个用例的路径) --html=report.html:生成 HTML 格式的测试报告

  • 规范测试用例发现规则:自定义测试文件、类、函数的命名模式。

  • 配置插件:指定测试报告格式、注册自定义标记(marker)等。

常用配置项

pytest.ini 的核心配置放在 [pytest] 段落下,以下是常用配置项:

配置项 作用说明
testpaths 指定测试文件所在的目录(相对路径),pytest 会优先在这些目录中查找测试用例。
python_files 定义测试文件的命名规则(默认:test_*.py *_test.py)。
python_classes 定义测试类的命名规则(默认:Test*,即类名以 Test 开头)。
python_functions 定义测试函数的命名规则(默认:test_*,即函数名以 test_ 开头)。
addopts 设置默认命令行参数(如 -s 显示打印输出、-v 详细模式)。
markers 注册自定义标记(如 smoke: 冒烟测试),避免运行时出现未注册标记警告。

示例:完整的 pytest.ini

假设项目结构如下:

project/
├── pytest.ini       # 配置文件
├── tests/           # 测试目录
│   ├── test_login.py
│   ├── test_order.py
└── src/             # 源码目录

3.1 编写

通过pytest --help拿到配置信息

pytest配置文件
说明:使用配置文件后可以快速的使用配置的项来选择执行哪些测试模块
注意:
1.在Windows系统下,pytest配置文件中,不允许写注释信息
2.一个工程内只需要一个pytest配置文件,并且需要保证文件名正确
3.一般情况,只需要将pytest配置文件,置于工程根目录下
4.配置有pytest配置文件的工程,只需要打开命令行,输入pytest指令,即可执行测试

image-20250917200222619

3.2 执行

默认设置

说明:测试用例文件名/测试类名/测试方法名均为Test/test开头

[pytest]
testpaths =./case
addopts =-S
python_files =test*.py
python_classes = Test*
python_functions = test*

自定义规则

说明:测试用例文件名/测试类名/测试方法名,需要根据具体项目进行设置.以下以 Hm为例

[pytest]
testpaths =./case
addopts =-S
python_files =hm*.py
python_classes = hm*
python_functions = hm*
扩展:指定单个文件/类/方法执行

可以通过定死某一参数

[pytest]
testpaths =./case
addopts =-S
python_files = demo_casel.py
python_classes = DemoDemo1
python_functions = demo_method1

4.python常用插件

4.1HTML插件

安装步骤:

方式一 --》pip install pytest-html

方式二 --》 Project -- Interpreter

使用步骤

在配置文件中的命令行参数中增加--hml=用户路径/report.html

示例 pytest.ini
addopts =-S
         --html=./report/report.html --self-contained-html
         
结果
--html=./report/report.html   在项目目录下会对一个report文件夹,里面有个report.html 即为测试报告

--self-contained-html 将CSS文件内嵌到文件中

image-20250917203510622

image-20250917203602627

4.2控制函数执行顺序

应用场景 现实生活中,如果想下订单,必须先登录,我们可以通过插件的形式来控制函数执行的顺序。

安装 使用命令pip3 install pytest-ordering进行安装

使用 1.标记于被测试函数,@pytest.mark.run(order=x) 2.根据order传入的参数来解决运行顺序 3.order值全为正数或全为负数时,运行顺序:值越小,优先级越高 4.正数和负数同时存在:正数优先级高

#扩展:序号支持正数和负数,以及正负混合 #1,纯正数:数越小,优先级越高[掌握] #2,纯负数:数越小,优先级越高[了解] #3.正负混合:正数先按照顺序执行,负数最后执行[了解]

注意:对测试类同样有效

示例

import pytest

@pytest.mark.run(order=1)
class TestDemo(object):
# 示例测试类
#语法:Opytest.mark.run(order=序号)
#注意:run(order=序号)没有代码提示,需要手写!

    @pytest.mark.run(order=3)
    def test_method1(self):
    # 测试方法1
        print('测试方法1')
        
    @pytest.mark.run(order=2)
    def test_method2(self):
    # 测试方法2
        print('测试方法2')
        
    @pytest.mark.run(order=1)
    def test_method3(self):
    # 测试方法3
        print('测试方法3')

输出:

collected 3 items

test.py 测试方法3
.测试方法2
.测试方法1

4.3失败重试

应用场景 自动化测试脚本可能会使用到网络,如果网络不好可能最终会使脚本不通过。像这种情况可能并不是脚本本身的问题,仅仅是因为网络忽快忽慢,那么我们可以使用失败重试的插件,当失败后尝试再次运行。一般情况最终成功可以视为成功,但最好进行进行排查时候是脚本问题。

安装 使用命令pip3 install pytest-rerunfailures进行安装 使用 在配置文件中的命令行参数中增加--reruns -n

n 是次数

addopts =-S
		 -html=./report/test_report.html--self-contained-html
		 --reruns 1

示例 pytest.ini addopts=-s-reruns 3

5.pytest高级功能

跳过测试
    #语法:pytest.mark.skipif(符合的条件,reason='跳过的原因')
    #注意:reason=不能省略,否则报错!
# 跳过功能

import pytest

version = 25
#模拟软件版本号变量

class TestDemo(object):
# 示例测试类
    def test_method1(self):
    # 测试方法
        print('测试方法1')
              

    #语法:pytest.mark.skipif(符合的条件,reason='跳过的原因')
    #注意:reason=不能省略,否则报错!
    # @pytest.mark.skipif(version>= 25,'xxx')#错误样例

    @pytest.mark.skipif(version>=25,reason='当前版本不执行')#正确样例
    def test_method2(self):
    # 测试方法”
        print('测试方法2')
          
    def test_method3(self):
    # 测试方法
        print('测试方法3') 
        


@pytest.mark.skipif(version>=25,reason='当前版本不执行')#同样可以跳过,正确样例
class TestDemo2(object):
    # 示例测试类
    def test_method(self):
        print('测试类2->测试方法')
单个参数化

应用场景 登录功能都是输入用户名,输入密码,点击登录。但登录的用户名和密码如果想测试多个值是没有办法用普通的操作实现的。数据参数化可以帮我实现这样的效果。 方法名

#数据参数化
#参数:
#	argnames:参数名
#	argvalues:参数对应值,类型必须为可迭代类型,一般使用list
@pytest.mark.parametrize(argnames, argvalues,indirect=False,ids=None,scope=None)

一个参数使用方式 1.argnames为字符串类型,根据需求决定何时的参数名 2.argvalues为列表类型,根据需求决定列表元素中的内容 3.在测试脚本中,参数,名字与argnames保持一致 4.在测试脚本中正常使用

argvalues列表有多少个内容,这个脚本就会运行几次

示例

import pytest

class TestLogin:
	@pytest.mark.parametrize("name["xiaoming","xiaohong"])
	def test_a(self,name):
		print(name)
		assert 1

                             
结果
scripts/test_login.py xiaoming
.xiaohong
# pytest参数化功能:单个参数
import pytest

class TestDemo(object):
# 示例测试类
#语法:@pytest.mark.parametrize('参数变量',['数值1',「数值2',··.])
    @pytest.mark.parametrize('name',['小明','小刚','小红'])
    def test_method(self,name):
    # 测试方法
        print('获取的名字是:', name)
    
if __name__ ==  '__main__' :
    pytest.main(['-s','test.py'])
多个参数化
# 参数化:多个参数
import pytest


class TestDemo(object):
    # 示例测试类”
    #语法:@pytest.mark.parametrize('参数1,参数n',[(数据1-1,数据n-1),(数据1-2,数据n-2),.·.])
    # 注意:
    #1.多个参数必须置于同一个字符串内!
    #2.数据格式必须是:[(),()]或者[[],[]]
    # 拓展:另一种写法
    # @pytest.mark.parametrize('参数1','参数n',[(数据1-1,数据n-1),(数据1-2,数据n-2),.·.])
    @pytest.mark.parametrize('name,pwd', [('admin',123456),('test',654321)])
    def test_method(self,name,pwd):
    # "测试方法”
        print('账号:{}密码:{}'.format(name,pwd))
    
if __name__ ==  '__main__' :
    pytest.main(['-s','test.py'])
通过方法引入数据
def build_test_data():
	构造测试数据函数
	#中间代码略
return[('admin'123456)("test’,654321),('xxx','yyy')]
                         
@pytest.mark.parametrize('name,pwd', build_test_data())
def test_method(self,name,pwd):
断言

assert 表达式

# 断言
import pytest

def add_func(num1,num2):
# …加法函数”…”
    return num1 + num2

class TestDemo(object):
    # 示例测试类
    def test_method(self):
    # 加法测试方法
    #调用被测函数
        result = add_func(1,2)
    #断言判断结果
        assert 4 == result
    
if __name__ ==  '__main__' :
    pytest.main(['-s','test.py'])

PO模式介绍

目标

1.深入理解方法封装的思想
2.能够使用方法封装的思想对代码进行优化
3.深入理解PO模式的思想
4.熟练掌握PO模式的分层思想

PO模式学习思路

采用版本迭代的方式来学习,便于对不同版本的优缺点进行对比和理解。

  • V1:不使用任何设计模式和单元测试框架
  • V2:使用PyTest管理用例
  • V3:使用方法封装的思想,对代码进行优化
  • V4:采用PO模式的分层思想对代码进行拆分
  • V5:对PO分层之后的代码继续优化
  • V6:PO模式深入封装,把共同操作提取封装到父类中,子类直接调用父类的方法

1.1选择的测试用例 ●账号不存在 1.点击首页的‘登录’链接,进入登录页面 2.输入一个不存在的用户名 3.输入密码 4.输入验证码 5.点击登录按钮 6.获取错误提示信息 ●密码错误 1.点击首页的‘登录’链接,进入登录页面 2.输入用户名 3.输入一个错误的密码 4.输入验证码 5.点击登录按钮 6.获取错误提示信息

V0
账号不存在测试用例
from time import sleep
from selenium import webdriver
driver = webdriver.ChromeC)
driver.get("http://127.0.0.1/')
driver.maximize_window()#窗口最大化
driver.implicitly_wait(10)#隐式等待
#1,点击首页的‘登录’链接,进入登录页面
driver.find_element_by_link_text('登录').click()
#2,输入一个不存在的用户名
driver.find_element_by_id('username').send_keys('13811110001')
#3.输入密码
driver.find_element_by_id('password').send_keys('123456')
#4,输入验证码
driver.find_element_by_id('verify_code').send_keys('8888')
#5,点击登录按钮
driver.find_element_by_name('sbtbutton').click()
           
# 账号不存在测试用例
from time import sleep
from selenium import webdriver

driver = webdriver.Chrome()
driver.get('http://127.0.0.1/')
           
driver.maximize_window()#窗口最大化
driver.implicitly_wait(10)#隐式等待

#1,点击首页的‘登录’链接,进入登录页面
driver.find_element_by_link_text('登录').click()

#2,输入一个不存在的用户名
driver.find_element_by_id('username').send_keys('13811110001')
#3.输入密码
driver.find_element_by_id('password').send_keys('123456')
#4,输入验证码
driver.find_element_by_id('verify_code').send_keys('8888')
#5,点击登录按钮
driver.find_element_by_name('sbtbutton').click()

#6,获取错误提示信息
#获取元素文本值:元素对象.text
msg =driver.find_element_by_class_name('layui-layer-content').text
print('错误信息为:',msg)
sleep(3)
driver.quit()
           
if __name__ ==  '__main__' :
    pytest.main(['-s','test.py'])
V1–整合多个脚本至同一个测试用例中
# 整合多个脚本至同一个测试用例中
import pytest


class TestLogin(object):
    	# 登录测试类
    
    def set_method():
        # 开始函数
        pass
    
    def teardown_method():
        # 结束函数
        pass
       
    def test_account_does_not_exist(self):
    	# 账号不存在测试方法…
        pass

    def test_wrong_password(self):
    	# 密码错误测试方法”
        pass
    
if __name__ ==  '__main__' :
    pytest.main(['-s','test.py'])
V2–使用pytest管理

完成代码的前置、后置和中间的过度

import pytest
import driv


class TestLogin(object):
    	# 登录测试类
        
    def set_class():
    	# 开始类
   		 pass
    
    def teardown_class():
        # 结束类
        pass
    
    def set_method():
        # 开始函数
        pass
    
    def teardown_method():
        # 结束函数
        pass
       
    def test_account_does_not_exist(self):
    	# 账号不存在测试方法…
        pass

    def test_wrong_password(self):
    	# 密码错误测试方法”
        pass
    
if __name__ ==  '__main__' :
    pytest.main(['-s','test.py'])
V3-方法封装

方法封装 目标 1.深入理解方法封装的思想 2.能够使用方法封装的思想对代码进行优化

1.方法封装

方法封装:是将一些有共性的或多次被使用的代码提取到一个方法中,供其他地方调用。

封装的好处:

  • 避免代码冗余

  • 容易维护

  • 隐藏代码实现的细节

目的:用最少的代码实现最多的功能

方法封装套路

1.确定方法的存放位置:		找位置
2.给方法起个合适名字:		起名字
3.放入要封装的代码内容:		放代码
4.确认是否需要参数和返回值:		确必要
5.调用封装好的方法使用:		做调用

示例

utils.py 公共方法模块
# 公共方法模块:习惯命名utils

# 公共方法:打开、关闭浏览器
from time import sleep
from selenium import webdriver


class DriverUtil(object):
 # 浏览器对象管理类
driver = None #声明driver,为判断创造对象
    def get_driver(self):
    #获取浏览器对象方法
	#说明:为了防止在同一次测试过程中,调用获取浏览器对象方法时,
	#都会创建一个新的浏览器对象,因此有必要先判断对象是否存在,不存在时在创建!
	if self.driver is None:
        self.driver= webdriver.Chrome()
        self.driver.get('http://127.0.0.1/')
        self.driver.maximize_window()#窗口最大化
        self.driver.implicitly_wait(10) # 隐式等待
    return self.driver #

    def quit_driver(self):
        # 退出浏览器对象方法
        # 说明: 必须保证浏览器对象存在,才能执行退出
    if self.driver:
        sleep(3)
        self.driver.quit()
        #保险手段:移除对象后,初始化对象变量,以备下次使用
        self.driver = None
        
# 应用自身测试代码
if __name__ ==  '__main__' :
    my_driver =DriverUtil()
    my_driver.get_driver()
    sleep(2)
    my_driver.quit_driver()

以类方法的形式封装:

#声明driver,为判断创造对象
# utr  ils.py 公共方法模块
# 公共方法模块:习惯命名utils

# 公共方法:打开、关闭浏览器
from time import sleep
from selenium import webdriver


class DriverUtil(object):
 # 浏览器对象管理类
    __drive = None
    @classmethod
    def get_driver(cls):
    #获取浏览器对象方法
	#说明:为了防止在同一次测试过程中,调用获取浏览器对象方法时,
	#都会创建一个新的浏览器对象,因此有必要先判断对象是否存在,不存在时在创建!
        if cls.__driver is None:      
         cls.__driver = webdriver.Chrome()
         cls.__driver.get('http://127.0.0.1/')
         cls.__driver.maximize_window()#窗口最大化
         cls.__driver.implicitly_wait(10) # 隐式等待
        return cls.__driver #

    @classmethod    
    def quit_driver(cls):
        # 退出浏览器对象方法
        # 说明: 必须保证浏览器对象存在,才能执行退出
        if cls.__driver:
            sleep(3)
            cls.__driver.quit()
            #保险手段:移除对象后,初始化对象变量,以备下次使用
            cls.__driver = None
        
# 应用自身测试代码
if __name__ ==  '__main__' :
    # my_driver =DriverUtil()
    # my_driver.get_driver()
    # sleep(2)
    # my_driver.quit_driver()
    
    DriverUtil.get_driver()
    sleep(2)
    DriverUtil.quit_driver()

引用:

def setup_class(self):
    ...
	self.driver =DriverUtil.get_driver()#获取浏览器对象
    
def teardown_class(self):
    ...
	DriverUtil.quit_driver()#退出浏览器对象

目标 1.深入理解PO模式的思想 2.熟练掌握PO模式的分层思想

进入PO模式前

目前存在的问题

在做UI自动化时定位元素特别依赖页面,一旦页面发生变更就不得不跟着去修改定位元素的代码。

举例:假设要对一个元素进行点击操作,而且会经常对该元素进行操作,那么你就可能会编写多 处如下代码

·driver.find_element_by_id(“login-btn”).click()·

存在的问题: ·如果开发人员修改了这个元素的id,这时候你就不得不修改所有对应的代码

PO是PageObject的缩写,PO模式是自动化测试项目开发实践的最佳设计模式之一。

核心思想是通过对界面元素的封装减少冗余代码,同时在后期维护中,若元素定位发生变化,只需要调整页面元素封装的代码,提高测试用例的可维护性、可读性。

PO模式可以把一个页面分为三层,对象库层、操作层、业务层。

  • 对象库层:封装定位元素的方法。

  • 操作层:封装对元素的操作。

  • 业务层:将一个或多个操作组合起来完成一个业务功能。比如登录:需要输入帐号、密码、点击登录三个操作。

引入PO模式的好处

引入PO模式前 存在大量冗余代码

PO设计模式

PO 设计模式 说明:PO模式又可以叫POM,是UI自动化测试中一个非常流行的设计模式(代码套路)

核心:将元素定位及操作和业务逻辑,拆分三个层面(每个层面对应一个单独的类),然后通过调用完成 最终的测试执行行为的过程

三个层面:对象库层/操作层/业务层

V4 页面封装

封装: 定义类 –》获取对象 –》定义操作

image-20250918022211735

PO页面元素封装步骤

1.对应页面创建页面PO代码文件,命名规则:页面功能_page·py,例如首页:index_page.py
2.定义三个类:对象层(XxxPage)/操作层(XxxHandle)/业务层(XxxTask)

3.对象层:
	1>init方法中获取浏览器对象
	2>自定义方法:封装元素定位方法
	3>封装元素定位方法需要添加返回值!
4.操作层:
	1>init方法获取对象层对象,根据类名写对象变量名
	2>自定义方法:封装元素操作方法
5.业务层:
	1>init方法获取操作层对象,根据类名写对象变量名
	2>自定义方法:封装测试业务逻辑
	
6,在测试用例文件中,实例化业务层对象,调用测试业务方法,执行测试

对象层封装

classIndexPage(object):
	"""首页对象层"""
    def __init__(self):
    	self.driver=DriverUtil.get_driver(#获取浏览器对象
            
    def find_login(self):
    	"""定位登录方法"""
    #1 self.driver.find_element_by_link_text('登录')
            
    #2  self.driver=DriverUtil.get_driver()#获取浏览器对象
    return self.driver.find_element_by_link_text('登录')

操作层封装:

class IndexHandle(object):
    """首页操作层"""
    def __init__(self):
    	self.index_page=IndexPageO#获取对象层对象
        
    def click_login(self):
    	"点击登录方法””
    	#  =IndexPage()
        # element.find_login().click()
    	self.index_page.find_login().click()

业务层封装:

class IndexTask(object):
    """首页业务层"""
    def __init__(self):
    	self.index_handle=IndexHandle()#获取操作层对象
    
    def go_to_login(self):
    """跳转登录页面方法"""
    self.index_handle.click_login()
V5 代码优化

对象层代码结构优化

参数 对象化

def _init__(self):
    self.driver=DriverUtil.get_driverO#获取浏览器对象
    #说明:将元素的定位方法及特征值封装成属性,能够实现集中管理目标元素的定位方法及特征值
    self.name = (By.ID, 'username') # 用户名

    def find_username(self):
        """定位用户名方法"""
    # return self.driver.find_element_by_id('username')
    # return self.driver.find_element(By.ID,'username')
        return self.driver.find_element(self.name[0],self.name[1])

操作层代码结构优化

输入层,在执行输入操作前,应该先执行清空操作

def input_username(selfname):
"""输入用户名方法"""
#说明:在执行输入操作前,应该先执行清空操作
self.login_page.find_username().clear
self.login_page.find_username().send_keys(name)

Pytest详解,常用插件介绍以及批量安装插件(pytest-html, pytest-xdist, allure等)

Pytest默认测试用例运行规则以及基础应用

Pytest运行时常用参数详解

Pytest两种运行模式(命令行,主函数)

Pytest全局配置文件pytest.ini配置详解以及markers测试用例分组执行

Pytest跳过测试用例

Pytest测试用例的执行顺序控制器

Pytest前后置,夹具,固件

Pytest核心固件Fixture详解以及应用实战

Pytest核心固件Fixture结合conftest.py应用实战

Pytest执行过程底层顺序详解

Pytest之基础路径base_url设置(测试环境,开发环境,生产环境等)

Pytest断言应用

Pytest集成allure-pytest生成企业级Allure测试报告

企业级Allure报告定制

企业级Allure报告独立访问

接口自动化测试用的YAML文件应用详解

接口自动化测试用的YAML文件读写及清空

Pytest通过@pytest.mark.parametrize实现数据驱动


Similar Posts

上一篇 Postman

下一篇 Markdown语法

Comments