一、pytest 测试框架
1. pytest 测试框架特点
- 简单易用
-
- 简洁的测试函数定义:pytest使用简单的函数来定义测试用例,相比其他一些测试框架,不需要复杂的类继承或者特定的方法命名规则。例如,下面是一个简单的pytest测试函数,用于测试一个加法函数:
def add(a, b):return a + bdef test_add():assert add(2, 3) == 5
-
- 自动发现测试用例:pytest能够自动发现测试文件和测试函数。默认情况下,它会在当前目录及其子目录中查找以
test_
开头或_test
结尾的Python文件,并将其中以test
开头的函数作为测试用例。这种自动发现机制使得测试用例的组织和执行变得非常方便,不需要手动去逐个添加测试用例。例如,只要将测试函数按照上述命名规则放置在合适的目录下,运行pytest
命令就可以自动执行所有的测试用例。
- 自动发现测试用例:pytest能够自动发现测试文件和测试函数。默认情况下,它会在当前目录及其子目录中查找以
- 丰富的断言机制
-
- 直观的断言语句:pytest使用Python的
assert
语句来进行断言,这使得断言非常直观和易于理解。可以直接在测试函数中使用assert
来检查函数返回值、变量状态等是否符合预期。例如,在测试一个列表排序函数时:
- 直观的断言语句:pytest使用Python的
def sort_list(lst):return sorted(lst)def test_sort_list():input_list = [3, 1, 2]result = sort_list(input_list)assert result == [1, 2, 3]
-
- 详细的断言错误信息:当断言失败时,pytest会提供详细的错误信息,帮助定位问题。它会显示断言表达式、实际值和预期值,使得开发者能够快速理解测试失败的原因。例如,如果上述
test_sort_list
中的断言失败,pytest会显示类似“assert [3, 2, 1] == [1, 2, 3]”的错误信息,清楚地展示了实际值和预期值的差异。
- 详细的断言错误信息:当断言失败时,pytest会提供详细的错误信息,帮助定位问题。它会显示断言表达式、实际值和预期值,使得开发者能够快速理解测试失败的原因。例如,如果上述
- 灵活的测试夹具(Fixtures)
-
- 共享测试资源:测试夹具是pytest的一个重要特性,它可以用于设置测试所需的资源,并且在多个测试用例之间共享。例如,可以定义一个夹具来创建数据库连接对象,然后在多个测试数据库操作的用例中使用这个夹具。下面是一个简单的夹具示例,用于返回一个示例列表:
import pytest@pytest.fixture
def sample_list():return [1, 2, 3]def test_list_length(sample_list):assert len(sample_list) == 3
-
- 夹具的作用域管理:pytest允许控制夹具的作用域,包括函数级、类级、模块级和会话级。可以根据测试资源的性质和使用场景来选择合适的作用域。例如,对于数据库连接夹具,可能希望将其作用域设置为模块级,这样在整个模块的测试过程中只创建一次数据库连接,提高测试效率。
- 夹具的参数化:可以对夹具进行参数化,使得夹具能够根据不同的参数值返回不同的资源。这在需要测试多种情况或者不同配置下的功能时非常有用。例如,在测试一个乘法函数时,可以定义一个参数化的夹具来提供不同的乘数组合:
import pytest@pytest.fixture(params=[(2, 3), (4, 5)])
def multiply_params(request):return request.paramsdef test_multiply(multiply_params):a, b = multiply_paramsassert a * b == a * b
通过pytest.mark.parametrize 进行参数化:
import pytest@pytest.mark.parametrize("num1, num2, expected", [(1, 2, 3),(4, 5, 9),(6, 7, 13)
])
def test_addition(num1, num2, expected):assert num1 + num2 == expected
- 插件生态系统丰富
-
- 功能扩展方便:pytest拥有庞大的插件生态系统,可以通过安装插件来扩展其功能。这些插件涵盖了从测试报告生成、与其他工具集成到性能测试等多个方面。例如,
pytest - html
插件可以生成HTML格式的测试报告,pytest - xdist
插件可以实现测试用例的分布式执行,提高测试速度。 - 社区支持活跃:由于pytest是一个广泛使用的测试框架,其插件的开发和维护得到了社区的积极支持。这意味着可以很容易地找到解决问题的方法、获取插件的更新以及了解最新的插件功能。同时,也可以根据自己的需求开发自定义插件,并分享给社区。
- 功能扩展方便:pytest拥有庞大的插件生态系统,可以通过安装插件来扩展其功能。这些插件涵盖了从测试报告生成、与其他工具集成到性能测试等多个方面。例如,
- 支持多种测试类型
-
- 单元测试:pytest非常适合编写单元测试,能够方便地测试单个函数或者类的方法。可以通过断言来检查函数的返回值、异常抛出等情况。例如,在测试一个除法函数时,可以检查除数为0时是否抛出正确的异常:
def divide(a, b):if b == 0:raise ZeroDivisionError("除数不能为0")return a / bdef test_divide():with pytest.raises(ZeroDivisionError):divide(1, 0)
-
- 集成测试和功能测试:除了单元测试,pytest也可以用于集成测试和功能测试。可以通过模拟外部服务、数据库等方式,或者直接在真实的环境中运行测试用例,来测试多个组件之间的集成情况或者整个系统的功能。例如,在测试一个Web应用的用户登录功能时,可以使用测试客户端来模拟用户登录操作,并检查登录后的状态。
2. pytest 介绍及安装
- pytest安装
-
- 在线安装 pip install pytest
- pycharm安装
-
-
- 打开pycharm中
- 在Pycherm打开settings
- 选择"project- interpreter"
- 找到pytest,选择Specify version,可以勾选不同版本
- 点击Install package
-
- 判断是否安装成功:
-
- pip show pytest
- pycharm
3. pytest 类定义及方法定义及运行
说明:
- pytest测试类的定义:类名必须是以Test开头
- pytest测试方法的定义:方法名必须是以test开头
执行:
- 运行方式一:
-
- 在pycharm中右击方法名或者类名可以直接执行对应测试类或者测试方法
- 运行方式二:
-
- 通过pytest -s -v 测试文件名称 (-s 允许在测试运行时输出测试函数中的
print
语句的输出结果; -v使测试输出更详细,包括每个测试用例的名称。)
- 通过pytest -s -v 测试文件名称 (-s 允许在测试运行时输出测试函数中的
import pytestdef sub(a, b):return a-bclass TestSub: # 定义的类名必须是以Test开头def test_sub_01(self): # 定义的测试方法必须是以test开头result = sub(2, 1)print(result)def test_sub_02(self):result = sub(4, 2)print(result)
4. pytest 断言
说明:
- pytest里面的断言方法就只有一个 assert 表达式
import pytestdef sub(a, b):return a-bclass TestSub: # 定义的类名必须是以Test开头def test_sub_01(self): # 定义的测试方法必须是以test开头result = sub(2, 1)print(result)assert result == 1 # 判断相等# assert result != 4 # 判断不相等# assert result # 判断为True# assert False # 判断为False# assert "a" in "abc" # 判断包含# assert "a" not in "abc" # 判断不包含# assert result is Noneassert result is not None
5. pytest 方法级别的fixture
说明:
- pytest方法级别的fixture是针对每个测试方法,在执行测试方法前会执行fixture初始化的操作,在执行完测试方法后,执行fixture销毁的操作。
初始化: def setup(self)
销毁: def teardown(self): - 如下图:每个方法执行之后都打印了执行时间
代码示例:
import time
import pytestdef sub(a, b):return a-bclass TestSub: # 定义的类名必须是以Test开头# 添加方法级别的初始化方法def setup(self):print("测试用例开始执行时间:", time.strftime("%Y-%m-%D %H:%M:%S"))# 添加方法级别的销毁方法def teardown(self):print("测试用例结束时间:", time.strftime("%Y-%m-%D %H:%M:%S"))def test_sub_01(self): # 定义的测试方法必须是以test开头result = sub(2, 1)print(result)def test_sub_02(self):result = sub(3, 1)print(result)
6. pytest 类级别的fixture
说明:
- pytest 类级别的fixture是针对每个测试类的初始化和销毁的操作
-
- 类级别初始化: def setup_class(self)
- 类级别销毁: def teardown_class(slef)
- 注意:fixture的方法必须写在测试类当中,不能写在测试类外面
- 如下图:只在类执行的前后打印了执行时间
代码示例:
import time
import pytestdef sub(a, b):return a-bdef add(c, d):return c-dclass TestSub: # 定义的类名必须是以Test开头# 添加类级别的初始化方法def setup_class(self):print("测试用例开始执行时间:", time.strftime("%Y-%m-%D %H:%M:%S"))# 添加类级别的销毁方法def teardown_class(self):print("测试用例结束时间:", time.strftime("%Y-%m-%D %H:%M:%S"))def test_sub_01(self): # 定义的测试方法必须是以test开头result = sub(2, 1)print(result)def test_sub_02(self):result = sub(3, 1)print(result)
7. pytest 配置文件
说明:
Pytest 配置文件可以改变 Pytest 的运行方式,通过这个固定的文件,读取配置信息,按指定的方式去运行。一般放在项目的根目录,命名为pytest.ini【pytest.ini tox.ini setup.cfg (取任意一个)】,不能使用任何中文符号,包括汉字、空格、引号、冒号等等。不管是主函数模式运行,还是命令行模式运行,都会自动读取这个全局配置文件。
功能解释(注意实际代码里不能加注释):
- [pytest] # 标识当前配置文件是pytest的配置文件
- addopts = -s -v # 标识pytest执行时增加的参数
- testpaths = ./scripts # 匹配搜索的目录
- python_files = test_*.py # 匹配测试文件
- python_classes = Test* # 匹配测试类
- python_functions = test_* # 匹配测试方法
[pytest]
addopts = -s -v
testpaths = ./scripts
python_files = test_*.py
python_classes = Test*
python_functions = test_*
二、Pytest常用插件
1. pytest测试报告
- 安装pytest测试报告插件
-
- 在pycharm中直接安装
- 使用方法
-
- 在配置文件中加上如下代码(注意需要手动创建一个repot文件)
addopts = -s -v --html=report/report.html
- 执行代码后会自动生成报告,用浏览器打开后如图展示
2. 控制用例执行顺序
- 说明:
-
- pytest 未指定顺序的情况下是根据测试方法从上到下的顺序来执行的
- 可以通过 pytest-ordering 插件来控制pytest测试方法执行的顺序
- 使用
order
标记可以更灵活地控制测试用例的执行顺序,但在实际应用中应谨慎使用,因为过度依赖特定的执行顺序可能会导致测试的脆弱性和不可靠性。通常,除非有特殊的必要,建议让pytest
按照默认的随机顺序执行测试用例,以更全面地检测代码的稳定性。
- 安装:
-
- 在线安装: pip install pytest-ordering
- pycharm (方法同上)
- 使用@pytest.mark.run(order=x) # x 表示的是整数,(既可以是负数也可以是正数)
- 规则:
-
- 全为负数或者正数时,值越小,优先级越高
- 既有正数,又有负数,那么正数优先级高
- 没有确定执行顺序的用例优先于负数
@pytest.mark.last # 设置用例最后执行
3. 失败重试
- 说明:
-
pytest-rerunfailures
是一个pytest
的插件,用于重新运行失败的测试用例。- 重试对于由于环境不稳定或其他临时因素导致的偶尔失败的测试用例非常有用,可以提高发现真正问题的概率。
- pytest-rerunfailures 安装
-
- 在线安装:pip install pytest-rerunfailures
- pycharm(方法同上)
- 重试
-
- 使用:在addopts参数行中增加对应的参数项:
-s
:允许在测试运行时输出测试函数中的print
语句的输出结果。-v
:使测试输出更详细,包括每个测试用例的名称。--reruns 3
:指定如果测试用例失败,将自动重新运行最多 3 次,当重复执行成功时,就不会再重复执行。
addopts = -s -v --reruns 3
三、 pytest高级用法
1. 跳过
- 方法一
-
@pytest.mark.skipif(condition, reason=None)
是pytest
中的一个标记,用于根据指定的条件跳过测试用例。
-
condition
是一个布尔表达式,如果其结果为True
,则测试用例将被跳过。reason
是一个可选参数,用于提供跳过测试用例的原因说明。
-
- 示例:
import pytest
import sys@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python 3.8 or higher")
def test_some_feature():# 测试代码assert True
在上述示例中,如果运行测试的 Python 版本小于 3.8 ,则 test_some_feature
测试用例将被跳过,并显示指定的原因 Requires Python 3.8 or higher
- 方法二
-
@pytest.mark.skip("版本")
这种用法会直接跳过对应的测试用例,并给出 "版本" 作为跳过的原因说明。
import pytest@pytest.mark.skip("此测试在当前版本中不适用")
def test_version_incompatible():# 测试代码assert True
当运行测试时,test_version_incompatible
这个测试用例将被跳过,并显示 "此测试在当前版本中不适用" 作为跳过的原因。
2. 数据参数化
- pytest参数化实现: @pytest.mark.parameterize(argnames, argvalues)
-
@pytest.mark.parameterize(argnames, argvalues)
用于在pytest
中对测试用例进行参数化。
-
argnames
是一个字符串,指定参数的名称,多个参数用逗号分隔("username, password")
-
argvalues
是一个可迭代对象,其中每个元素都是一组参数值。参数个数要与argvalues里面的测试数据的个数要相同,否则会报错。 [("13700001111","123124"),("13800011111","123456")]
- 示例
import pytest@pytest.mark.parametrize("num1, num2, expected", [(1, 2, 3),(4, 5, 9),(7, 8, 15)
])
def test_add(num1, num2, expected):assert num1 + num2 == expected
在上述示例中,test_add
函数被参数化了。pytest
会自动为每组参数值运行一次测试用例。这样可以减少重复代码的编写,提高测试的覆盖度和效率。
3. 静态方法
在 Python 中,静态方法(staticmethod
)和普通方法(实例方法)主要有以下区别:
-
- 调用方式
-
-
- 普通方法需要通过类的实例来调用,因为普通方法可以访问和操作实例的属性。
- 静态方法可以通过类直接调用,无需创建类的实例。
-
-
- 参数
-
-
- 普通方法的第一个参数通常是
self
,通过self
可以访问实例的属性和其他方法。 - 静态方法不接受
self
参数,也不能访问实例的属性和方法。
- 普通方法的第一个参数通常是
-
-
- 与实例的关系
-
-
- 普通方法与特定的实例相关联,其操作依赖于实例的状态。
- 静态方法与类的实例无关,它更多地是对类的一种通用功能的封装。
-
-
- 代码示例:
class MyClass:def __init__(self, value):self.value = valuedef normal_method(self): # 普通方法print(f"Value in normal method: {self.value}")@staticmethoddef static_method(): # 静态方法print("This is a static method")# 创建实例并调用普通方法
instance = MyClass(10)
instance.normal_method() # 直接通过类调用静态方法
MyClass.static_method()
4. 文件夹和包的区别
4.1. 文件夹(Directory)
-
- 定义与用途
-
-
- 在Python中,文件夹是一种用于存储文件和其他文件夹的容器。它主要用于组织和管理文件系统中的文件。例如,你可以创建一个名为“data”的文件夹来存放各种数据文件,如CSV文件、JSON文件等;或者创建一个“scripts”文件夹来存放Python脚本文件。
-
-
- 创建方法
-
-
- 在Python中,可以使用操作系统相关的模块来创建文件夹。例如,使用
os
模块。以下是一个简单的示例,用于在当前目录下创建一个名为“new_folder”的文件夹:
- 在Python中,可以使用操作系统相关的模块来创建文件夹。例如,使用
-
import os
folder_name = "new_folder"
try:os.mkdir(folder_name)print(f"{folder_name}文件夹创建成功。")
except FileExistsError:print(f"{folder_name}文件夹已经存在。")
- 这里使用了`os.mkdir()`方法来创建文件夹。这个方法会在当前目录下创建一个指定名称的文件夹。如果文件夹已经存在,会抛出`FileExistsError`异常。
- 访问和使用
-
- 文件夹中的文件可以通过文件路径来访问。例如,如果在“new_folder”中有一个名为“example.txt”的文件,你可以使用相对路径“new_folder/example.txt”或者绝对路径(如果知道的话)来打开和读取这个文件。在Python中,当你使用文件操作相关的函数(如
open()
函数来打开文件)时,就需要提供文件的路径。
- 文件夹中的文件可以通过文件路径来访问。例如,如果在“new_folder”中有一个名为“example.txt”的文件,你可以使用相对路径“new_folder/example.txt”或者绝对路径(如果知道的话)来打开和读取这个文件。在Python中,当你使用文件操作相关的函数(如
4.2. 包(Package)
-
- 定义与用途
-
-
- 包是一种特殊的文件夹,用于组织Python模块。它是Python模块层次结构中的一个概念,目的是更好地管理代码的组织结构,使得代码更具模块化和可维护性。一个包可以包含多个模块(
.py
文件),并且可以有子包。例如,在一个大型的Web应用开发项目中,可以有一个名为“web_app”的包,里面包含“models”(数据模型模块)、“views”(视图模块)、“controllers”(控制器模块)等子包,每个子包又包含相关的模块。
- 包是一种特殊的文件夹,用于组织Python模块。它是Python模块层次结构中的一个概念,目的是更好地管理代码的组织结构,使得代码更具模块化和可维护性。一个包可以包含多个模块(
-
-
- 创建方法
-
-
- 要创建一个包,首先需要创建一个文件夹,这个文件夹的名称就是包的名称。然后在这个文件夹中必须包含一个特殊的文件
__init__.py
。这个文件可以是空的,它的存在表示这个文件夹是一个Python包。例如,要创建一个名为“my_package”的包,步骤如下: - 首先创建一个名为“my_package”的文件夹。
- 在“my_package”文件夹中创建一个
__init__.py
文件。可以使用文本编辑器打开一个新文件,然后保存为__init__.py
,内容可以暂时为空。 - 现在“my_package”就是一个合法的Python包了。你可以在这个包中添加模块(
.py
文件),例如创建一个名为“my_module.py”的模块,将其放在“my_package”文件夹中。
- 要创建一个包,首先需要创建一个文件夹,这个文件夹的名称就是包的名称。然后在这个文件夹中必须包含一个特殊的文件
-
-
- 访问和使用
-
-
- 要使用包中的模块,需要通过正确的导入语句。例如,如果要在另一个Python文件中使用“my_package”中的“my_module.py”模块,可以使用以下导入方式:
- 如果“my_module.py”中有一个函数
my_function()
,可以这样导入和使用:
-
# 方式一:导入整个模块
import my_package.my_module
my_package.my_module.my_function()# 方式二:从模块中导入特定的函数或类等
from my_package.my_module import my_function
my_function()
- 包的层次结构可以通过点号(
.
)来表示。
在导入语句中,通过点号来指定模块在包中的位置。
同时,Python会根据sys.path
(系统路径)来查找包和模块的位置。
如果包不在sys.path
所包含的路径中,需要将包所在的路径添加到sys.path
中,才能正确地导入和使用。