pytest 是一个非常灵活且强大的测试框架,它支持简单的单元测试到复杂的功能测试。显著特点是其简洁的语法,可以无需继承 TestCase 类直接使用函数来编写测试用例,并通过 assert语句 进行断言。还支持参数化测试、插件系统以及丰富的 assertion rewriting 功能,使编写测试更加便捷和直观。我们可以使用它进行自动化测试。
pytest 本身有一些内置的 mark 装饰器,需要介绍的包括:
@pytest.mark.skip
@pytest.mark.skipif
@pytest.mark.xfail
@pytest.mark.parametrize
@pytest.mark.usefixtures
这些装饰器分别提供不同的功能。
PS: 本文基于pytest 8.3.3,建议使用PyCharm编写并运行测试套件,方便。
参数化测试
@pytest.mark.parametrize 用于参数化测试,允许你对同一个测试函数传入多个参数集,参数集是一个元组,所有参数集构成一个列表:
# C:\PythonTest\Test\test_module1.py
import pytest
# 第一个参数是字符串,里面定义需要传递的参数。
# 第二个参数是列表,列表中一个元组是一个参数集合
@pytest.mark.parametrize("n, expected", [
(1 + 2, 3),
(2 + 2, 4),
(3 * 3, 9)
])
def test_math(n, expected):
assert n == expected
python .\app.py ,运行结果
========================================== test session starts =======================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 3 items
Config\test_mod.py::test_math[3-3] PASSED [ 33%]
Config\test_mod.py::test_math[4-4] PASSED [ 66%]
Config\test_mod.py::test_math[9-9] PASSED [100%]
=========================================== 3 passed in 0.01s ========================================================
通过标记测试组(类),可以参数化测试组中的所有测试用例:
# C:\PythonTest\Test\test_module1.py
import pytest
pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)])
class TestClass:
def test_simple_case(self, n, expected):
assert n + 1 == expected
def test_weird_simple_case(self, n, expected):
assert (n * 1) + 1 == expected
python .\app.py ,运行结果:
========================================== test session starts =======================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 4 items
Config\test_mod.py::TestClass::test_simple_case[1-2] PASSED [ 25%]
Config\test_mod.py::TestClass::test_simple_case[3-4] PASSED [ 50%]
Config\test_mod.py::TestClass::test_weird_simple_case[1-2] PASSED [ 75%]
Config\test_mod.py::TestClass::test_weird_simple_case[3-4] PASSED [100%]
=========================================== 4 passed in 0.01s ========================================================
要参数化模块中的所有测试,可以分配给 pytestmark 全局变量:
# C:\PythonTest\Test\test_mod.py
import pytest
pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (2, 4)])
def test_one(n, expected):
assert n+n == expected
def test_two(n, expected):
assert n+n == expected
python .\app.py ,运行结果:
========================================== test session starts =======================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 4 items
Config\test_mod.py::test_one[1-2] PASSED [ 25%]
Config\test_mod.py::test_one[2-4] PASSED [ 50%]
Config\test_mod.py::test_two[1-2] PASSED [ 75%]
Config\test_mod.py::test_two[2-4] PASSED [100%]
=========================================== 4 passed in 0.01s ========================================================
要获取多个参数化参数的所有组合,可以堆叠 parametrize装饰器
# C:\PythonTest\Test\test_module1.py
import pytest
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
pass
python .\app.py ,运行结果:
========================================== test session starts =======================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 4 items
Config\test_mod.py::test_foo[2-0] PASSED [ 25%]
Config\test_mod.py::test_foo[2-1] PASSED [ 50%]
Config\test_mod.py::test_foo[3-0] PASSED [ 75%]
Config\test_mod.py::test_foo[3-1] PASSED [100%]
=========================================== 4 passed in 0.01s ========================================================
会按照 x=0/y=2
x=1/y=2
x=0/y=3
x=1/y=3
的顺序耗尽参数。
也可以在参数化中标记单个测试实例:
# C:\PythonTest\Test\test_module1.py
import pytest
@pytest.mark.parametrize(
"n,expected",
[("3+5", 8), ("2+4", 6), pytest.param("6*7", 42, marks=pytest.mark.user)],
)
def test_eval(n, expected):
assert eval(n) == expected
python .\app.py -m user ,运行结果:
========================================== test session starts =======================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 3 items / 2 deselected / 1 selected
Config\test_mod.py::test_eval[6*7-42] PASSED [100%]
==================================== 1 passed, 2 deselected in 0.01s ===================================================
读取 json 文件中的数据进行参数化:
新增 Tools 和 Data 目录,Tools 存放工具类,Data 存放数据文件:
Project/
│
├── Config/
│ └── pytest.ini # 自定义 mark 标记
│
├── Data/
│ └── parametrize.json # 根据策略模块
│
├── Package/ # 程序目录
│ ├── __init__.py # 包初始化文件,可以定义一些变量或执行一些操作。当然里面什么都不写也可以。
│ ├── module1.py # 应用程序模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│ └── module2.py # 应用程序模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│
├── Test/ # 测试用例目录
│ ├── __init__.py # 包初始化文件
│ ├── test_module1.py # 测试 module1 的测试用例
│ └── test_module2.py # 测试 module2 的测试用例
├── Tools/ # 工具目录
│ ├── __init__.py # 包初始化文件
│ └── files_processor.py # 文件处理工具
├── app.py # 项目启动文件
├── requirements.txt # 项目依赖项列表
└── README.md # 项目说明文档
在 parametrize.json 中设计数据结构,根据 @pytest.mark.parametrize 的特性可以这样设计:
{
"test1":{
"param": "x,y,z",
"data": [
[1,8,9],
[2,5,7],
[11,9,20]
]
},
"test2": {
"param": "n,m",
"data": [
[2,2],
[3,3],
[4,4]
]
}
}
在 parametrize.json 中写入所有测试用例需要的数据,例如有 test_test1和 test_test2 的测试用例,就在文件中添加 test1 和 test2 的键对值。json 中没有元组的概念,使用列表代替。
根据数据文件的结构,在 files_processor.py 中编写处理文件的工具类。示例处理 parametrize.json 文件:
# files_processor.py
# 设计文件处理工具
import json
from abc import ABC, abstractmethod
import os
# 定义 FileHandler 接口
class FileStrategy(ABC):
@abstractmethod # 任何继承自 FileStrategy 的类都必须实现read方法
def _read(self, path):
"""Read Data from the file."""
pass
# 实现Json文件处理策略
class JsonHandler(FileStrategy):
def _read(self, path):
with open(path, 'r', encoding='utf-8') as file:
return json.load(file)
# 定义读取Json文件用于 parametrize 的方法
def parametrize_data(self, path, case_name):
"""
:param case_name: 测试用例的名称(去掉 test_ ),它是一个字符串
:param path: Json文件的路径
:return: 返回参数名和值的结果对象
用于 @pytest.mark.parametrize(param, data,)
"""
json_data = self._read(path)
json_param = json_data[case_name]["param"]
data1 = json_data[case_name]["data"]
data2 = [tuple(x) for x in data1]
return json_param, data2
# 文件处理器工厂
class FileHandlerFactory:
@staticmethod
def get_handler(file_type: str):
if file_type == 'json':
return JsonHandler()
# 可以添加更多的条件分支来支持其他类型的文件处理器
raise ValueError(f"Unsupported file type: {file_type}")
# 文件处理器客户端
class FileProcessor:
@staticmethod
def process_file(function_name, file_name, **kwargs):
"""
:param function_name:
parametrize_data:读取json文件中的数据,应用 @pytest.mark.parametrize 进行参数化。json 内数据格式应是
{"test1":{"param":"x,y,z","data":[[1,8,9],[2,5,7],[11,9,20]]},"test2":{"param":"n,m","data":[[2,2],[3,3],[4,4]]}}
还需指定 case_name = "测试用例的名称(去掉 test_ 前缀)"
:param file_name: 文件的名字(带后缀)
:return: 对应方法的返回值值 或 None
"""
if function_name == "parametrize_data":
file_type = "json"
handler = FileHandlerFactory.get_handler(file_type)
script_dir = os.path.dirname(os.path.abspath(__file__))
while os.path.basename(script_dir) != 'Tools':
script_dir = os.path.dirname(script_dir)
project_root = os.path.dirname(script_dir)
return handler.parametrize_data(project_root+"\\Data\\"+file_name, **kwargs)
# 可以添加更多的条件分支来支持其他文件处理器
else:
return False, None
# 测试
if __name__ == "__main__":
processor = FileProcessor()
param, data = processor.process_file('parametrize_data', 'parametrize.json', case_name="test1")
print(param)
print(data)
设计测试用例,从 test_eval.json 中读取数据进行参数化:
# C:\PythonTest\Test\test_module1.py
import pytest
from Tools.files_processor import FileProcessor
processor = FileProcessor()
param, data = processor.process_file('parametrize_data', 'parametrize.json', case_name="test1")
@pytest.mark.parametrize(param,data,)
def test_test1(x, y, z):
assert x+y == z
param, data = processor.process_file('parametrize_data', 'parametrize.json', case_name="test2")
@pytest.mark.parametrize(param,data,)
def test_test2(n,m):
assert n == m
python .\app.py ,运行结果:
================================================ test session starts =================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collecting ... collected 5 items
Config\test_mod.py::test_test1[1-8-9] PASSED [ 20%]
Config\test_mod.py::test_test1[2-5-7] PASSED [ 40%]
Config\test_mod.py::test_test1[11-9-20] PASSED [ 60%]
Config\test_mod.py::test_test2[2-2] PASSED [ 80%]
Config\test_mod.py::test_test2[3-3] PASSED [100%]
================================================= 5 passed in 0.01s ==================================================
上述示例没有对异常情况进行处理,仅作为示例,请根据实际情况编写工具。
如果测试时需要处理其它文件,可以在 files_processor.py 中构建其它工具,比如想要对 json 文件进行其它操作,就可以在 JsonHandler 类中编写其它方法。想要处理其它文件,比如 xlsx 文件,可以新建 XlsxHandler 类并构建方法。
跳过测试用例
pytest 内置 @pytest.mark.skip 和 @pytest.mark.skipif 用于跳过测试用例。 被 @pytest.mark.skip 装饰的测试用例,运行时会被跳过;被 @pytest.mark.skipif 装饰的测试用例,满足条件时被跳过:
@pytest.mark.skip:
# C:\PythonTest\Test\test_module1.py
import pytest
@pytest.mark.skip(reason="这个测试用例还没有写好")
def test_one():
assert 1 == 1
def test_two():
assert 1 == 1
python .\app.py ,运行结果:
================================================ test session starts =================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 2 items
Config\test_test.py::test_one SKIPPED(这个测试用例还没有写好) [ 50%]
Config\test_test.py::test_two PASSED [100%]
=============================================== 2 passed in 0.01s ======================================================
@pytest.mark.skipif:
# C:\PythonTest\Test\test_module1.py
import pytest
@pytest.mark.skipif(3 < 8, reason="满足 3<8 的条件,此测试被跳过")
def test_one():
assert 1 == 1
def test_two():
assert 1 == 1
python .\app.py ,运行结果:
============================================= test session starts =====================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 2 items
Config\test_test.py::test_one SKIPPED (满足 3<8 的条件,此测试被跳过) [ 50%]
Config\test_test.py::test_two PASSED [100%]
========================================== 1 passed, 1 skipped in 0.01s =================================================
可以把 @pytest.mark.skipif 结合参数化使用:
# C:\PythonTest\Test\test_module1.py
import pytest
from Tools.files_processor import FileProcessor
processor = FileProcessor()
param, data = processor.process_file('parametrize_data', 'parametrize.json', case_name="test1")
if param:
skip_if_code = False
else:
skip_if_code = True
param = "x,y,z"
data = [(1,1,1)]
@pytest.mark.skipif(skip_if_code, reason="没有指定的function_name")
@pytest.mark.parametrize(param,data,)
def test_test1(x, y, z):
assert x+y == z
param, data = processor.process_file('parametrize', 'parametrize.json', case_name="test2")
if param:
skip_if_code = False
else:
skip_if_code = True
param = "n,m"
data = [(1,1)]
@pytest.mark.parametrize(param,data,)
@pytest.mark.skipif(skip_if_code, reason="没有指定的function_name")
def test_test2(n,m):
assert n == m
python .\app.py ,运行结果:
============================================= test session starts =====================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 4 items
Config\test_mod.py::test_test1[1-8-9] PASSED [ 25%]
Config\test_mod.py::test_test1[2-5-7] PASSED [ 50%]
Config\test_mod.py::test_test1[11-9-20] PASSED [ 75%]
Config\test_mod.py::test_test2[1-1] SKIPPED (没有指定的function_name) [100%]
========================================== 3 passed, 1 skipped in 0.01s =================================================
标记已存在的问题
如果测试被 @pytest.mark.xfail 标记并且测试失败,测试会被记录为预期失败,而不是完全失败。用于已经知道测试的功能存在问题,进行标记:
# C:\PythonTest\Test\test_module1.py
import pytest
@pytest.mark.skip(reason="这个测试用例还没有写好")
def test_one():
assert 1 == 1
def test_two():
assert 1 == 1
python .\app.py ,运行结果:
============================================= test session starts =====================================================
platform win32 -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 -- C:\PythonTest\.venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: C:\PythonTest\Config
configfile: pytest.ini
collected 4 items
collected 2 items
Config\test_mod.py::test_one XPASS (已知问题,还未修复) [ 50%]
Config\test_mod.py::test_two PASSED [100%]
========================================== 1 passed, 1 xpassed in 0.01s =================================================
@pytest.mark.usefixtures 会在介绍 @pytest.fixture 的篇章中说明。
THEEND
© 转载需要保留原始链接,未经明确许可,禁止商业使用。CC BY-NC-ND 4.0
...