python测试-企业级测试框架

python测试-企业级测试框架

https://www.processon.com/view/link/697057a23e4afc5c3659a3c4

https://www.bilibili.com/video/BV1rDdHYCEUP?spm_id_from=333.788.player.switch&vd_source=379b5659b7b00bb6caa4cadf9cc37ad6

https://www.bilibili.com/video/BV1pgXBB1E1d?spm_id_from=333.788.player.switch&vd_source=379b5659b7b00bb6caa4cadf9cc37ad6&p=8

什么是测试框架?

根据大量的测试实践,抽象出来一个常用工具集合,包含大量组件或功能,以及经过验证的方法论

常用工具和组件

用例发现:自动化的从各目录、各文件种收集测试用例
用例管理:根据需求对用例进行筛选、忽略、跳过等操作

环境管理:在用例执行前后,自动完成某些操作,构造合适的执行条件(如:执行前打开浏览器,执行后关闭浏览器)

用例执行:执行用例种的测试步骤
断言:执行用例时,判定执行结果是否符合预期

测试报告:生成测试报告

java:junit,testng

python:pytest,unittest,RF

unittest是python自带的,且pytest完全兼容unittest

unittest代码语言风格像java,因为参考了junit

pytest拥有更多的插件1400多

经过验证的方法论

经过验证的方法论
用例之间相互隔离
使用断言宣告结果
为每个用例单独收集IO和日志
数据驱动测试、行为驱动测试、关键字驱动测试等

冒烟测试、步进测试、回归测试、增量测试

pytest

pytest基本用法

用例规则

目录:没有要求

文件:文件名以test_x开头或者x_test结尾

类名:Test开头

类和目录和文件都算是容器,不算做是用例,类的方法或者函数才是用例

函数(方法)名:test开头

参数和返回值:不能有返回值、参数不能随便定义(只能在数据驱动或者fixture时才能使用)

断言方式

assert 表达式:为False时,会报错AssetError

执行demo

pytest

pytest -v

pytest 具体文件.py

mark
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pytest

def test_pass(): # 用例通过
pass

def test_fail():
assert False # 直接用例失败

@pytest.mark.skip # 直接跳过
def test_skip():
assert False

@pytest.mark.xfail # 预期失败,这里通过了预期外的通过,还是失败
def test_xpass():
pass

@pytest.mark.xfail # 预期失败,符合预期,还是失败
def test_xfail():
assert False
1
2
3
4
5
6
7
8
>>>src\test_demo.py .FsXx

>>>src\test_demo.py .FsXx -v
src/test_demo.py::test_pass PASSED [ 14%]
src/test_demo.py::test_fail FAILED [ 28%]
src/test_demo.py::test_skip SKIPPED (unconditional skip) [ 42%]
src/test_demo.py::test_xpass XPASS [ 57%]
src/test_demo.py::test_xfail XFAIL

pytest进阶技巧

标记mark

目的:让用例与众不同

自定义标记

筛选用例主要靠自定义标记

先注册
后标记
再筛选
内置标记

不用注册

skip:无条件跳过
xfail:预期失败
parametrize:参数化测试(数据驱动测试)

根据数据的数量和内容,决定用例的数量和内容

数据驱动->测试,测试驱动->开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pytest
def add(a,b):
return a+b

@pytest.mark.parametrize(
"a,b",# 参数名
[ # 参数值
(1,1),
(-1,-1),
("a","b"),
(["a"],[4])
]
)
def test_add(a,b):
c = a+b
assert c == add(a,b)

只执行具体文件里的用例

1
2
3
4
5
6
7
8
9
10
11
12
13
(venv) D:\code\PYTHON_TEST>pytest -v src\test_dataDriveTest.py
==================== test session starts ====================
platform win32 -- Python 3.12.2, pytest-9.0.3, pluggy-1.6.0 -- D:\code\PYTHON_TEST\venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\code\PYTHON_TEST
collected 4 items

src/test_dataDriveTest.py::test_add[1-1] PASSED [ 25%]
src/test_dataDriveTest.py::test_add[-1--1] PASSED [ 50%]
src/test_dataDriveTest.py::test_add[a-b] PASSED [ 75%]
src/test_dataDriveTest.py::test_add[a3-b3] PASSED [100%]

===================== 4 passed in 0.03s =====================
只执行特定筛选的用例
1
2
3
4
5
6
7
8
9
(venv) D:\code\PYTHON_TEST>pytest -m parametrize
==================== test session starts ====================
platform win32 -- Python 3.12.2, pytest-9.0.3, pluggy-1.6.0
rootdir: D:\code\PYTHON_TEST
collected 11 items / 7 deselected / 4 selected

src\test_dataDriveTest.py .... [100%]

============== 4 passed, 7 deselected in 0.02s ==============
1
2
3
4
5
6
7
8
9
10
11
12
13
(venv) D:\code\PYTHON_TEST>pytest -m parametrize -v
==================== test session starts ====================
platform win32 -- Python 3.12.2, pytest-9.0.3, pluggy-1.6.0 -- D:\code\PYTHON_TEST\venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\code\PYTHON_TEST
collected 11 items / 7 deselected / 4 selected

src/test_dataDriveTest.py::test_add[1-1] PASSED [ 25%]
src/test_dataDriveTest.py::test_add[-1--1] PASSED [ 50%]
src/test_dataDriveTest.py::test_add[a-b] PASSED [ 75%]
src/test_dataDriveTest.py::test_add[a3-b3] PASSED [100%]

============== 4 passed, 7 deselected in 0.03s ==============

用ai生成高质量的测试数据

usefixtures:使用夹具

夹具fixture

作用:

  • 实现setup(相当于jemeter中的前置和后置操作,或者unittest中的setup )/teadown机制
  • 实现注入机制

作用域:

  • 在同一作用域内的用例,复用注入结果

conftest:

  • 跨文件共享fixtures
创建

名字、参数、返回值

使用

通过参数使用

通过标记使用

demo

通过参数使用fixture和通过标记使用fixture,实现前置和后置操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pytest

@pytest.fixture()
def f():
# 前置操作
print("用例开始执行了....")

# 给用例传递内容
yield "来自fixture的数据"

# 后置操作
print("用例执行结束了!")

@pytest.mark.usefixtures("f")
def test_abc():
pass

def test_123(f):
pass

实现数据注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pytest

@pytest.fixture()
def f():
# 前置操作
print("用例开始执行了....")

# 给用例传递内容
yield "来自fixture的数据"

# 后置操作
print("用例执行结束了!")

@pytest.mark.usefixtures("f")
def test_abc(f): # 也可以传函数名,获得注入数据
pass

def test_123(f):
print(f)

pytest -v 可以在终端看到print输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(venv) D:\code\PYTHON_TEST\src>pytest test_fixture.py -vs
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.2, pytest-9.0.3, pluggy-1.6.0 -- D:\code\PYTHON_TEST\venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\code\PYTHON_TEST\src
collected 2 items

test_fixture.py::test_abc 用例开始执行了....
PASSED用例执行结束了!

test_fixture.py::test_123 用例开始执行了....
来自fixture的数据
PASSED用例执行结束了!


====================================================================== 2 passed in 0.03s =======================================================================
案例

1.通过 fixture 设置环境变量或修改全局状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import os
import pytest

@pytest.fixture()
def setup_environment():
"""设置环境,不需要返回值"""
# 前置操作
os.environ["TEST_MODE"] = "ACTIVE"
print("设置环境变量...")

yield

# 后置操作
os.environ.pop("TEST_MODE", None)
print("清理环境变量...")

@pytest.mark.usefixtures("setup_environment")
def test_environment():
# 可以直接访问被设置的环境
assert os.environ.get("TEST_MODE") == "ACTIVE"
print("测试可以使用被设置的环境")

1.通过类属性(在类测试中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest

class TestExample:
"""在类测试中共享数据"""

_shared_data = None

@pytest.fixture(autouse=True) # autouse=True 自动应用到所有测试
def setup_class_data(self):
# 设置类属性
TestExample._shared_data = "共享的数据"
print("设置类数据...")

yield

# 清理
TestExample._shared_data = None
print("清理类数据...")

def test_use_shared_data(self):
# 可以通过类属性访问
print(f"访问共享数据: {TestExample._shared_data}")
assert TestExample._shared_data == "共享的数据"

3.通过 request 上下文(高级用法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import pytest

@pytest.fixture()
def config_data(request):
"""通过 request 上下文传递数据"""
data = {"key": "value", "count": 42}

# 如果需要,可以将数据存储在 request 上下文中
request.cls.shared_data = data # 对于类测试

yield data

# 清理
if hasattr(request, 'cls'):
request.cls.shared_data = None

@pytest.mark.usefixtures("config_data")
class TestWithSharedData:
"""整个测试类使用同一个 fixture"""

def test_one(self):
# 通过类属性访问
assert self.shared_data["key"] == "value"
print(f"Test 1: {self.shared_data}")

def test_two(self):
assert self.shared_data["count"] == 42
print(f"Test 2: {self.shared_data}")
1
2
3
4
5
6
1. pytest 的测试执行机制
当 pytest 执行测试时:
会先创建测试类的实例
然后调用 fixture
fixture 通过 request.cls设置类属性
测试方法通过 self可以访问这个类属性
作用域

实现复用效果,对fixture的结果数据复用。

如果的fixture的作用是创建测试数据,或者修改数据等(ui自动化或者web自动化测试也可以使用这个pytest框架)。之前的方式会是fixture重复执行。

function(默认)

局限于一个用例,其它用例不会复用

class

module

模块也就是文件

package

包也就是目录

session

全部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pytest

@pytest.fixture(scope='session')
def f():
# 前置操作
print("用例开始执行了....")

# 给用例传递内容
yield "来自fixture的数据"

# 后置操作
print("用例执行结束了!")

@pytest.mark.usefixtures("f")
def test_abc():
pass

def test_123(f):
print(f)
1
2
3
4
5
6
7
8
9
10
11
12
13
===================================================================== test session starts ======================================================================
platform win32 -- Python 3.12.2, pytest-9.0.3, pluggy-1.6.0 -- D:\code\PYTHON_TEST\venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\code\PYTHON_TEST\src
collected 2 items

test_fixture.py::test_abc 用例开始执行了....
PASSED
test_fixture.py::test_123 来自fixture的数据
PASSED用例执行结束了!


====================================================================== 2 passed in 0.01s =======================================================================
conftest夹具封装

跨文件共享fixtures

conftest.py

需要跨文件共享的fixture放到特定文件conftest.py中

1
2
3
4
5
6
7
8
9
10
11
12
import pytest

@pytest.fixture(scope='session')
def f():
# 前置操作
print("用例开始执行了....")

# 给用例传递内容
yield "来自fixture的数据"

# 后置操作
print("用例执行结束了!")

钩子hook(比较难,自动化测试不需要了解,测试开发需要)

pytest常用插件

日志

用途:把用例执行结果保存到日志文件

安装:pip intall pytest-result-log

配置:

1
2
3
4
5
log_file = ./pytest.log
log_file_level = infolog_file_format = %(levelname)-8s %(asctime)s [%(name)s:%(lineno)s]:%(message)s
log_file_date_format = %Y-%m-%d %H:%M:%s
result_log_level_separator = warning
result_log_level_verbose = info

报告

用途:生成HTML测试报告

安装:pip intall pytest-html

配置:–html=report.html –self-contained-html

并发执行

用途:并发执行用例

安装:pip intall pytest-xdist

配置:命令行参数

-n {0,1,2,3….n,auto}

注意:

  • 多线程额外增加资源
  • 多进程乱序
  • 多进程竞争资源(-s失效)

控制顺序

用途:定义用例执行顺序

安装:pip install pytest-order

配置:标记

1
2
3
4
5
6
7
@pytest.mark.order(5) #后执行
def test_abc():
pass

@pytest.mark.order(1) #先执行
def test_bbc():
pass

顺序规则:

  • 先执行有order的用例,再执行没有order的用例
  • 先执行order值小的,再执行值大的
  • order全局生效,可以跨文件、跨目录

更多

用途:生成allure数据文件

安装:命令行参数 –alluredir=temps –clean-alluredir

本插件只生成数据,不生成报告:

  • 创建目录temps
  • 清空目录内容
  • 再目录中创建数据文件

完善项目实战

之前使用脚本基于requests的用例测试,不太完善,首先用例之间没有隔离,如一个用例测试失败,后续中断执行。

pytest的fixture在接口自动化测试中可能不太明显,但还是要习惯使用。

创建fixture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import pytest,requests,jsonpath

@pytest.fixtrue()
def client():
s = requests.Session()
yield s


@pytest.fixture(scope='session')
def user_client():
s = requests.Session()
resp = s.request(
"post",
"xxx",
json={
"account":"xxx",
"password":"xxx"
}
)

token = jsonpath.jsonpath(resp.json(),'$.access_token'[0])
s.headers.update({"Authorization":f"Bearer {token}"})

yield s


def test_login(client):
resp = client.request(
method = "post",
url = "xxx",
json = {"account":"xxx",
"password":"xxx"}
)
assert resp.status_code == 200
assert 'access_token' in resp.json()

def test_list(user_client):
pass

def test_create(user_client):
pass

def test_check(user_client):
pass

def test_delete(user_client):
pass

def test_recheck(user_client):
pass

创建用例

创建数据

可以使用数据驱动

企业级封装

透明化接口客户端

发送请求时自动的完成日志记录、文件加载、接口mock等功能扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import logging
import requests
import json
import jsonpath
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)

class APIClient(requests.Session):
def __init__(self, base_url: str = ""):
super().__init__()
self.base_url = base_url
# 初始化日志格式(实际项目中应在外部配置)
self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.INFO)

# 确保不重复添加处理器
if not self.logger.handlers:
ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter('%(levelname)s %(asctime)s [%(module)s:%(lineno)d] : %(message)s'))
self.logger.addHandler(ch)

def request(self, method: str, url: str, **kwargs) -> requests.Response:
# 1. 构建完整URL
full_url = f"{self.base_url}{url}" if self.base_url and not url.startswith(("http://", "https://")) else url

# 2. 记录请求信息(关键点1:请求前日志)
self.logger.info(f">>>>> 目标接口: {method.upper()} {full_url}")

# 3. 记录请求参数(关键点2:详细参数日志)
if kwargs:
for k, v in kwargs.items():
# 特殊处理:JSON数据美化输出
if k == "json" and v is not None:
try:
v = json.dumps(v, ensure_ascii=False, indent=2)
except:
pass
self.logger.info(f">>>>> 请求参数{k}: {v}")

# 4. 发送请求(关键点3:捕获异常)
try:
response = super().request(method, full_url, **kwargs)

# 5. 记录响应信息(关键点4:响应后日志)
self.logger.info(f"<<<<< 状态码: {response.status_code}")

# 6. 处理响应头(关键点5:安全解码)
try:
headers_str = json.dumps(dict(response.headers), ensure_ascii=False)
self.logger.info(f"<<<<< 响应头: {headers_str}")
except:
self.logger.info(f"<<<<< 响应头: {response.headers}")

# 7. 处理响应体(关键点6:UTF-8解码+美化)
try:
# 尝试解析为JSON
resp_json = response.json()
body = json.dumps(resp_json, ensure_ascii=False, indent=2)
self.logger.info(f"<<<<< 响应正文: {body}")
except:
# 非JSON响应(如二进制文件)
content_type = response.headers.get('Content-Type', '')
if 'text' in content_type:
body = response.text[:1000] # 限制长度
self.logger.info(f"<<<<< 响应正文: {body}")
else:
self.logger.info(f"<<<<< 响应正文: [二进制内容]")

# 8. 添加自定义属性(用于后续测试)
response.logger = self.logger
response.full_url = full_url
return response

except requests.exceptions.RequestException as e:
self.logger.error(f"请求异常: {str(e)}")
raise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import pytest,requests,jsonpath
from 企业级封装 import APIClient

@pytest.fixtrue()
def client():
s = APIClient()
yield s


@pytest.fixture(scope='session')
def user_client():
s = APIClient()
resp = s.request(
"post",
"xxx",
json={
"account":"xxx",
"password":"xxx"
}
)

token = jsonpath.jsonpath(resp.json(),'$.access_token'[0])
s.headers.update({"Authorization":f"Bearer {token}"})

yield s


def test_login(client):
resp = client.request(
method = "post",
url = "xxx",
json = {"account":"xxx",
"password":"xxx"}
)
assert resp.status_code == 200
assert 'access_token' in resp.json()

def test_list(user_client):
pass

def test_create(user_client):
pass

def test_check(user_client):
pass

def test_delete(user_client):
pass

def test_recheck(user_client):
pass

透明化第三方插件

执行用例时自动的完成第三方插件加载和使用,以及其他功能扩展

使用yaml作为用例

YAML:

1.yaml完全兼容JSON,但更适合人类阅读和编辑
2.目前主流的配置格式(docker-compse、k8s、github等)

YAML用例:

1.隐藏了用例执行细节,简化了用例复杂度,便于新人上手和维护

2.用例和框架代码分离,便于对用例进行独立的版本控制,或跟随业务代码一

3.便于AI理解和生成,为后续介入AI大模型做准备

1
2
3
4
5
6
7
8
9
10
name:登录
steps:
- request:
method: post
url: http://api.fbi.com:9225/rest-v2/login/access_token
json:
email:bf@qq.com
password: bf123456
- response:
status_code: 200

阶段目标:

1.让框架加载yaml数据
2.让框架加载yaml用例

3.将框架封装成独立exe程序

接口、web、app自动化测试三合一封装

接入AI大模型