测试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:
创建
名字、参数、返回值
使用
通过参数使用
通过标记使用
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) 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.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: full_url = f"{self.base_url}{url}" if self.base_url and not url.startswith(("http://", "https://")) else url self.logger.info(f">>>>> 目标接口: {method.upper()} {full_url}") if kwargs: for k, v in kwargs.items(): 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}") try: response = super().request(method, full_url, **kwargs) self.logger.info(f"<<<<< 状态码: {response.status_code}") try: headers_str = json.dumps(dict(response.headers), ensure_ascii=False) self.logger.info(f"<<<<< 响应头: {headers_str}") except: self.logger.info(f"<<<<< 响应头: {response.headers}") try: resp_json = response.json() body = json.dumps(resp_json, ensure_ascii=False, indent=2) self.logger.info(f"<<<<< 响应正文: {body}") except: 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"<<<<< 响应正文: [二进制内容]") 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大模型