pytest 是一个非常灵活且强大的测试框架,它支持简单的单元测试到复杂的功能测试。显著特点是其简洁的语法,可以无需继承 TestCase 类直接使用函数来编写测试用例,并通过 assert语句 进行断言。还支持参数化测试、丰富的插件系统。
pytest自动化测试框架搭建
本系列探讨一下如何基于 pytest 构建自动化测试套件。
PS: 本文基于pytest 8.3.3
安装 pytest
在终端中安装:
pip install -U pytest
测试用例和断言
pytest 的设计思想围绕测试用例的发现、执行、管理和报告展开,其灵活性和可扩展性也主要服务于测试用例的高效编写和维护。
测试用例需要编写在前缀为名称 test_ 或 test 的模块(.py文件)中,作为测试用例的函数的前缀也需为 test 或 _test。每个测试用例中都需要至少一个断言(assert)。比如:
# test_mod.py
list_list = 'hello!'
def test_list_fail():
assert 'hello1!' == list_list, "两个值不同"
def test_list_pass():
assert 'hello!' == list_list, "两个值不同"
test_list_fail 和 test_list_pass 皆是测试用例。
assert 后接两个参数,第一个值用于判断,第二个值是字符串。第一个值为 True(真) ,则该测试用例继续进行直至结束;如果为 False(假) ,则测试失败,并且 pytest 会停止执行该测试用例、报告错误并打印第二个值。
第一个值可以是表达式、函数返回值、数值、布尔值等等,第二个值可以自定义信息,精准描述问题。
test_list_fail 和 test_list_pass 两个测试用例的结果如下:
============================= test session starts =============================
collecting ... collected 2 items
test_test.py::test_list_fail FAILED [ 50%]
test_test.py:3 (test_list_fail)
'hello1!' != 'hello!'
预期:'hello!'
实际:'hello1!'
<点击以查看差异>
def test_list_fail():
> assert 'hello1!' == list_list, "两个值不同"
E AssertionError: 两个值不同
E assert 'hello1!' == 'hello!'
E
E - hello!
E + hello1!
E ? +
test_test.py:5: AssertionError
test_test.py::test_list_pass PASSED [100%]
========================= 1 failed, 1 passed in 0.05s =========================
test_mod.py::test_list_fail FAILED 表示测试用例 test_list_fail 断言失败,测试用例未通过 。
test_mod.py::test_list_pass PASSED 表示测试用例 test_hanshu、test_list 断言成功,测试用例通过。
做下补充,第一个值可以如下:
- 比较表达式
# 等于
assert 10 == 5 # False
# 不等于
assert 10 != 5 # True
# 大于
assert 10 > 5 # True
# 小于
assert 10 < 5 # False
# 大于等于
assert 10 >= 5 # True
# 小于等于
assert 10 <= 5 # False
- 逻辑表达式
# 逻辑与
assert (10 > 5) and (5 < 10) # True
# 逻辑或
assert (10 > 5) or (5 > 10) # True
# 逻辑非
assert not (10 > 5) # False
- 成员表达式
# in 操作符
assert 5 in [1, 2, 3, 4, 5] # True
# not in 操作符
assert 6 not in [1, 2, 3, 4, 5] # True
- 身份表达式
a = [1, 2, 3]
b = [1, 2, 3]
c = a
# is 操作符
assert a is c # True,因为 a 和 c 引用同一个对象
# is not 操作符
assert a is not b # True,因为 a 和 b 是不同的对象
- 布尔值的隐式转换
# 空列表、空字符串、零等会被视为 False,而非空列表、非空字符串、非零等会被视为 True
# 空列表
assert [] # False
# 非空列表
assert [1, 2, 3] # True
# 空字符串
assert "" # False
# 非空字符串
assert "Hello" # True
# 零
assert 0 # False
# 非零
assert 10 # True
- 布尔值
# True
assert True
# False
assert False
测试用例组
应用有许多模块,每个模块的测试用例我们都可以写在对应模块文件里。模块又有很多功能,我们又如何对功能进行划分呢?
pytest 框架里有测试组概念,用类(class)表达,可以在类里编写功能的所有测试用例。
# C:\PythonTest\Test\test_mod.py
class AddUser:
def test_smoke(self):
x = "this"
assert "h" in x, "字符串中没有 h"
def test_error(self):
x = 1
y = 2
assert type(x) == type(y), "x 不等于 y"
运行结果:
========================= 1 failed, 1 passed in 0.05s =========================
PASSED [ 50%]FAILED [100%]
test_test.py:5 (AddUser.test_error)
1 != 2
预期:2
实际:1
<点击以查看差异>
self = <test_test.AddUser object at 0x000001E3ADA52C10>
def test_error(self):
x = 1
y = 2
> assert x == y, "x 不等于 y"
E AssertionError: x 不等于 y
E assert 1 == 2
test_test.py:9: AssertionError
还可以添加 DeleteUser、EditUser 等等测试组。
运行测试用例
编写测试用例后,可以在命令行中运行测试用例。以 Windows 为例:
- 终端中运行 C:\PythonTest\Test\test_mod.py 中所有测试用例:
pytest C:\PythonTest\Test\test_mod.py
- 终端中运行 C:\PythonTest\Test\test_mod.py 中的 test_list_fail 测试用例:
pytest C:\PythonTest\Test\test_mod.py::test_list_fail
- 在终端中运行 C:\PythonTest\Test\test_mod.py 中的 TestAdd 测试用例组:
pytest C:\PythonTest\Test\test_mod.py::TestClassOne
pytest test_mod.py::TestClassOne
- 在终端中运行 C:\PythonTest\Test\test_mod.py 中 TestAdd 中的 test_error 测试用例:***
pytest C:\PythonTest\Test\test_mod.py::TestClassOne::test_error
初步构建测试套件
结合本章内容,我们可以初步构建一个测试套件。目录结构可以如下:
Project/
│
├── Package/ # 程序目录
│ ├── __init__.py # 包初始化文件,可以定义一些变量或执行一些操作。当然里面什么都不写也可以。
│ ├── module1.py # 公共模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│ └── module2.py # 公共模块,比如连接数据库操作数据,接口请求等操作,推荐按功能封装成类
│
├── Test/ # 测试用例目录
│ ├── __init__.py # 包初始化文件
│ ├── test_module1.py # 测试 module1 的测试用例
│ └── test_module2.py # 测试 module2 的测试用例
├── app.py # 项目启动文件
├── requirements.txt # 项目依赖项列表
└── README.md # 项目说明文档
一个完整的套件,需要有个启动文件 app.py ,测试时运行 app.py 文件。
app.py 文件可以这样设计:
import pytest
import sys
import logging
import argparse
import os
# 自定义日志格式
LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s'
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
logger = logging.getLogger(__name__)
def run_tests(test_target: str)->int::
"""
运行pytest测试并返回退出码
Args:
test_target (str): 测试目标路径
Returns:
int: pytest退出码
"""
if not os.path.exists(test_target):
logger.error(f"测试目标路径不存在: {test_target}")
return 1
# 构建pytest参数
pytest_args = ["-v", test_target]
try:
logger.info(f"开始运行测试,目标路径: {test_target}")
exit_code = pytest.main(pytest_args)
# 退出码说明映射
exit_messages = {
0: "✅ 全部测试用例通过",
1: "⚠️ 部分测试用例未通过",
2: "❌ 测试过程中有中断或其他非正常终止",
3: "❌ 内部错误",
4: "❌ pytest无法找到任何测试用例",
5: "❌ pytest遇到了命令行解析错误"
}
logger.info(exit_messages.get(exit_code, f"❓ 未知的退出码: {exit_code}"))
return exit_code
except Exception as e:
logger.exception("运行测试时发生致命错误:")
logger.debug("异常详情:", exc_info=True)
return 1
def parse_arguments()-> argparse.Namespace:
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="使用指定的命令运行 pytest 测试")
parser.add_argument(
'test_target',
nargs='?',
type=str,
default="Test/",
help='指定测试目录/文件 (默认: Test/)'
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_arguments()
exit_code = run_tests(args.test_target)
sys.exit(exit_code)
项目根目录下,可以这样运行测试:
- 运行所有测试:
python app.py
- 运行特定文件:
python app.py Test/test_module1.py
- 运行特定模块文件中的特定测试用例:
python app.py Test/test_module1.py::test_case
- 运行特定文件中的特定测试组:
python app.py Test/test_module1.py::TestClass
- 运行特定文件中的特定测试组中的特定测试用例:
python app.py Test/test_module1.py::TestClass::test_case

4618

被折叠的 条评论
为什么被折叠?



