一、Pytest的简介
1.1 什么是Pytest?
Pytest是一个功能强大、灵活且易于使用的Python测试框架,它已经成为Python生态系统中最受欢迎的测试工具之一。与Python标准库中的unittest框架相比,Pytest提供了更加简洁、直观的语法和更丰富的功能集,使得编写、组织和运行测试变得更加高效和愉快。
Pytest最初由Holger Krekel于2004年创建,经过多年的发展,现在已经成为一个成熟、稳定的测试框架,被广泛应用于各种规模的Python项目中------从简单的脚本到大型企业级应用。
1.2 核心特点与优势
1.2.1 简洁直观的语法
Pytest最大的优势之一是其极简的测试编写方式 。您不需要继承任何特定的类,也不需要记住复杂的断言方法名。一个最简单的测试函数只需要以test_开头:
python
def test_addition():
assert 1 + 1 == 2
这种简洁性大大降低了学习成本,让开发者能够更专注于测试逻辑本身。
1.2.2 强大的断言系统
Pytest内置了智能断言重写机制,当断言失败时,它会自动提供详细的错误信息,包括变量的值和表达式的中间结果:
python
def test_complex_assertion():
result = calculate_something()
# 如果断言失败,pytest会显示result的完整值和期望值
assert result == expected_value
1.2.3 丰富的插件生态系统
Pytest拥有一个庞大且活跃的插件生态系统,目前有超过1000个官方和第三方插件可供选择。这些插件覆盖了各种测试需求:
- pytest-xdist:支持并行测试执行
- pytest-cov:生成代码覆盖率报告
- pytest-mock:提供mock和patch功能
- pytest-html:生成HTML格式的测试报告
- pytest-django 、pytest-flask:针对Web框架的专用插件
1.2.4 灵活的测试发现机制
Pytest具有智能的测试发现功能,能够自动识别测试文件和测试函数:
- 默认查找以
test_开头的文件 - 识别以
test_开头的函数和方法 - 识别以
Test开头的类(不需要继承unittest.TestCase) - 支持自定义的发现规则
1.2.5 详细的测试报告
Pytest提供清晰、详细的测试执行报告,包括:
- 测试通过/失败/跳过的统计信息
- 失败测试的详细堆栈跟踪
- 测试执行时间的分析
- 支持多种输出格式(文本、HTML、JUnit XML等)
1.3 适用场景
Pytest适用于各种测试场景:
1.3.1 单元测试(Unit Testing)
测试单个函数、方法或类的行为是否正确。Pytest的fixture机制特别适合为单元测试提供测试数据和环境准备。
1.3.2 集成测试(Integration Testing)
测试多个组件之间的交互是否正确。Pytest可以轻松组织复杂的测试套件,模拟外部依赖。
1.3.3 功能测试(Functional Testing)
测试整个应用的功能是否符合需求。结合Selenium、Playwright等工具,Pytest可以用于Web应用的功能测试。
1.3.4 API测试
测试RESTful API或GraphQL接口。结合requests、httpx等HTTP客户端库,Pytest可以构建完整的API测试套件。
1.3.5 数据库测试
测试数据库操作和ORM层。Pytest的fixture可以管理数据库连接和事务,确保测试的隔离性。
1.4 与其他测试框架的对比
1.4.1 与unittest对比
| 特性 | Pytest | unittest |
|---|---|---|
| 测试编写 | 函数式,无需继承类 | 必须继承TestCase类 |
| 断言语法 | 使用Python原生assert | 使用assertEqual等特定方法 |
| 夹具系统 | fixture机制,灵活强大 | setUp/tearDown方法 |
| 插件生态 | 丰富,超过1000个插件 | 有限 |
| 测试发现 | 自动,基于命名约定 | 需要手动组织 |
1.4.2 与nose2对比
Pytest在活跃度、社区支持和功能完整性方面都优于nose2。nose2已经基本停止维护,而Pytest仍在快速发展。
1.5 为什么选择Pytest?
- 提高开发效率:简洁的语法和强大的功能减少了编写和维护测试的时间成本。
- 改善代码质量:详细的测试报告和断言信息帮助快速定位问题。
- 支持持续集成:与Jenkins、GitLab CI、GitHub Actions等CI/CD工具无缝集成。
- 降低维护成本:良好的测试组织和fixture机制使得测试代码更易于维护。
- 活跃的社区支持:遇到问题时,有大量的文档、教程和社区讨论可供参考。
1.6 学习路径建议
对于初学者,建议按照以下路径学习Pytest:
- 基础语法:掌握测试函数编写、断言使用
- 运行测试:学习命令行参数和配置方式
- fixture机制:理解Pytest最强大的特性之一
- 参数化测试:掌握数据驱动的测试方法
- 插件使用:根据项目需求选择合适的插件
- 高级特性:学习标记、钩子函数、自定义配置等
Pytest不仅是一个测试工具,更是一种测试哲学------它鼓励编写简单、可读、可维护的测试代码,让测试成为开发过程中自然且愉快的一部分。通过本教程的学习,您将掌握Pytest的核心概念和实践技巧,能够为您的Python项目构建健壮、高效的测试体系。# 二、安装
bash
pip install pytest -i https://pypi.tuna.tsinghua.edu.cn/simple/
三、pytest编写规则(默认)
1)测试文件以test_开头(或者以_test结尾也行)
2)测试类以Test开头,并且类里面不能带有__init__初始化构造函数
3)测试函数以test_开头
4)断言使用基本的assert即可
四、pytest的运行方式
1)命令行运行方式(一般用于调试)
直接在命令行中输入"pytest",它将自动查找并执行当前目录及其子目录中的所有以test_开头的测试用例文件。
bash
pytest
运行指定目录的测试用例
指定要运行的目录,pytest将查找并运行该目录及其子目录下的测试用例
bash
pytest testcase/userManager
运行指定文件的测试
bash
pytest testcase/userManager/test_deleteUser.py
使用"-k"参数并提供测试用例或测试目录的名称 ,可以运行匹配该名称的测试用例。
指定目录
bash
pytest -k userManager
指定测试文件
bash
pytest -k test_deleteUser
指定运行测试函数
bash
pytest -k test_login_failed
参数说明:
- -s 表示输出调试信息,包括print信息
- -v 显示更加详细的信息
- -vs 把上面这两个参数一起使用
- -k 指定运行测试用例文件、目录
- -n 设置多线程/多进程数量并发去执行测试用例
- --reruns:设置测试用例失败重跑次数
- -m 运行分组的测试用例
2)主函数方式运行
主函数运行,用于执行pytest测试,它可以通过传递命令行参数来配置和控制测试的执行
python
if __name__ == '__main__':
pytest.main(['-s', '-v', '-k', 'test_add_user_01'])
3)通过读取pytest框架的配置文件--pytest.ini(做企业自动化推荐的用法)
"pytest.ini"文件是Pytest测试框架的配置文件,它允许你设置各种配置选项一自定义测试运行的行为,一般是在项目的根目录下创建"pytest.ini",文件名固定写法不能任意更改,pytest将在运行测试时读取改文件并应用其中的配置。
配置文件(pytest.ini)参数说明:
- **addopts:**用于在运行测试时传递额外的命令行选项
- **testpaths:**用于指定pytest在哪些目录中查找测试文件,它允许你定义一个或多个目录,告诉pytest在这些目录下寻找测试文件
- **python_files:**用于指定测试文件的命名匹配模式
- **python_classes:**用于指定测试类的命名匹配模式
- **python_functions:**用于指定测试函数的命名匹配模式
- **markers:**用于注册自定义标记
pytest.ini文件内容配置如下:
bash
[pytest]
# -n 设置多线程并发执行数量 --reruns 设置全局的测试用例失败重跑的次数 -m 运行分组的测试用例
addopts = -s -v -n 3 --reruns 3 -m "P1 or P3"
testpaths = ./testcase
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# 注册自定义标记
markers =
last
second
first
P1
P3
五、断言
pytest框架使用python的内置断言("assert"语句)来进行测试,除了基本的"assert"语句外,pytest还提供了一些丰富的断言工具。
python
import pytest
class TestAssertResult:
# 相等断言,判断两个值是否相等
def test_assert_eq(self):
assert 1 == 1, '断言失败,它们不相等'
def test_assert_eq_02(self):
exp = {"msg": "登录成功"}
response = {"msg": "登录成功"}
assert exp == response, '断言失败,它们不相等'
# 不相等断言
def test_assert_nq(self):
exp = {"msg": "登录成功"}
response = {"msg": "登录成功"}
assert exp != response, '断言失败,它们相等'
# 真假断言
def test_assert_bool(self):
import random
assert random.choice([True, False]), '真假断言失败'
# 成员关系断言
def test_assert_contains(self):
contains = "pytest"
contains_lst = ['pytest', 'unittest', 'python']
assert contains in contains_lst
if __name__ == '__main__':
pytest.main(['-vs', '-k', 'test_assert_result'])
六、pytest丰富的插件系统
1)并发执行(多线程/多进程),pytest提供一个插件--pytest_xdist提供一个-n选项设置多进程/多线程数量,使测试用例可以在多个进程环境下并发执行,提高测试效率。
安装插件
bash
pip install pytest-xdist
代码实现:
python
import pytest
from time import sleep
class TestLogin:
def test_login_success(self):
sleep(3)
print('登录成功场景')
def test_login_failed(self):
sleep(3)
print('登录失败场景')
def test_login_error(self):
sleep(3)
print('登录错误场景')
# if __name__ == '__main__':
# pytest.main(['-s', '-v', '-n=3'])
2)失败测试用例重跑
当我们做自动化测试时执行测试用例,遇到网络波动或其他不确定因素导致测试用例运行失败,这并不是测试失败,这时需要用到失败重试运行
安装插件
bash
pip install pytest-rerunfailures
代码实现
python
import random
import pytest
class TestLogin:
# reruns失败重跑的次数设置,reruns_delay:失败重跑的间隔时间设置
# 这是对单个测试用例进行失败重跑设置
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_login_success(self):
assert random.choice([True, False]), '测试失败'
print('登录成功场景')
def test_login_failed(self):
assert random.choice([True, False]), '测试失败'
print('登录失败场景')
def test_login_error(self):
print('登录错误场景')
if __name__ == '__main__':
pytest.main(['-s', '-v'])
3)测试用例执行顺序
因为在默认情况下,pytest会以特定的顺序执行测试用,可以使用pytest-ordering插件去自定义设置测试用例的执行顺序
安装插件
bash
pip install pytest-ordering
代码实现:
第一种方式:@pytest.mark.run(order=)
python
import pytest
class TestAddUser:
@pytest.mark.run(order=3)
def test_add_user_01(self):
print("新增用户01")
@pytest.mark.run(order=2)
def test_add_user_02(self):
print("新增用户02")
@pytest.mark.run(order=1)
def test_add_user_03(self):
print("新增用户03")
if __name__ == '__main__':
pytest.main(['-vs', '-k', 'test_adduser'])
第二种方式
python
import pytest
class TestAddUser:
# 需要现在pytest.ini配置文件注册自定义的标记
@pytest.mark.last
def test_add_user_01(self):
print("新增用户01")
@pytest.mark.second
def test_add_user_02(self):
print("新增用户02")
@pytest.mark.first
def test_add_user_03(self):
print("新增用户03")
if __name__ == '__main__':
pytest.main(['-vs', '-k', 'test_adduser'])
七、pytest分组执行和跳过执行
1)分组执行
在pytest中,可以使用标记(mark)或者参数化来实现测试用例的分组执行,分组执行是一种将测试用例按照特定的标记或者条件进行组织和运行的方法。
- 使用-m去执行分组的测试用例:pytest -vs -m "P1"
- 使用-m去执行多个组的测试用例:pytest -vs -m "P1 or P3"
python
import pytest
class TestDeleteUser:
@pytest.mark.P1
def test_delete_user_01(self):
print("删除用户01")
@pytest.mark.P3
def test_delete_user_02(self):
print("删除用户02")
2)跳过执行
在pytest中,可以使用@pytest.mark.skip装饰器或者@pytest.mark.skipif装饰器,以及可以使用@pytest.mark.xfail装饰器来跳过测试用例的执行。
pytest提供跳过执行用例的三种方式:
- @pytest.mark.skip:无条件跳过,在测试用例函数上直接使用,pytest执行时就会跳过该用例的执行
- @pytest.mark.skipif:有条件跳过,根据指定的条件跳过测试用例的执行
- @pytest.mark.xfail:用于标记测试用例为预期失败,当测试用例执行时不会统计为失败,而是作为预期失败标记
python
import pytest
class TestLogin:
def test_login_success(self):
print('登录成功场景')
@pytest.mark.skip
def test_login_failed(self):
print('登录失败场景')
num = 5
@pytest.mark.skipif(num == 5, reason="符合表达式条件,所以此用例跳过。。。")
def test_login_error(self):
print('登录错误场景')
@pytest.mark.xfail
def test_login_except(self):
assert 1 == 2
if __name__ == '__main__':
pytest.main(['-vs', '-k', 'test_skip_case'])
八、参数化处理
在pytest中,参数化是一种将相同的测试用例以不同的参数运行多次的机制,这可以帮助简化测试代码,使其更灵活和易维护,达到覆盖不同的测试场景。
它通过**@pytest.mark.parametrize**装饰器去实现参数化。
参数化的基本用法:
- 使用@pytest.mark.parametrize装饰器将参数传递给测试函数
- 指定参数名和参数值
- 在测试函数的参数中接收参数值,用于多次测试
- 语法:@pytest.mark.parametrize("params1,params2...",
iterable),第一个参数用于指定测试函数的参数名称,第二个参数是可迭代的对象,如:列表、元组、字典等。 - 测试函数接收的参数名必须要与参数化的第一个参数保持一致,参数个数也要保持一致
python
import pytest
from unit_tools.handle_data.yaml_handler import read_yaml
class TestParameter:
# 传递多个参数,通过列表类型的可迭代对象实现参数化
@pytest.mark.parametrize('user_name,password',
[('test01', 'admin123'), ('test01', 'admin222'), ('test02', 'admin123'),
('test*&&$', 'ad(*n$')])
def test_login(self, user_name, password):
print(user_name, password)
print('正确的用户和密码登录成功校验')
# 传递一个参数
@pytest.mark.parametrize('user_name', ['test01', 'test02', 'test03'])
def test_login02(self, user_name):
print(user_name)
print('正确的用户和密码登录成功校验')
# 通过元组类型的可迭代对象实现参数化
@pytest.mark.parametrize('user_name', ('test01', 'test02', 'test03'))
def test_login_03(self, user_name):
print(user_name)
print('正确的用户和密码登录成功校验')
# 通过字典类型的可迭代对象实现参数化
@pytest.mark.parametrize('user_name', {"user_name": "test01", "user_name02": "test02"})
def test_login_03(self, user_name):
print(user_name)
print('正确的用户和密码登录成功校验')
@pytest.mark.parametrize('api_info', read_yaml('../../data/login.yaml'))
def test_login_04(self, api_info):
print(f'获取到的接口信息:{api_info}')
if __name__ == '__main__':
pytest.main(['-k', 'test_parameter'])
九、前后置处理(setup/teardown/setup_class/teardown_class、Fixture)
在测试框架中,前后置是在在执行测试用例前和测试用例后执行一些额外的操作,这些操作可以用于初始化测试环境、准备测试数据,以确保测试的可靠性和一致性。
pytest框架提供三种方式做测试用例的前后置操作:
9.1 setup、teardown/setup_class、teardown_class
a. 函数级别的前后置,在每个测试函数开始和结束时都会执行一次前后置,用于函数级别的初始化和清理。
python
import pytest
class TestSetup:
def setup(self):
print('所有测试函数执行前先执行这个操作')
def test_case_01(self):
print('这个是第一个测试用例')
def test_case_02(self):
print('这个是第二个测试用例')
def test_case_03(self):
print('这个是第三个测试用例')
def teardown(self):
print('所有测试函数执行结束后先执行这个操作')
if __name__ == '__main__':
pytest.main(['-k','test_setup_teardown'])
b. 类级别的前后置,在每个测试类开始和结束时只执行一次前后置,用于类级别的初始化和清理
python
import pytest
class TestSetup:
def setup_class(self):
print('在测试类执行前先执行此方法')
def test_case_01(self):
print('这个是第一个测试用例')
def test_case_02(self):
print('这个是第二个测试用例')
def test_case_03(self):
print('这个是第三个测试用例')
def teardown_class(self):
print('在测试类执行结束后先执行此方法')
if __name__ == '__main__':
pytest.main(['-k','test_setup_teardown'])
9.2 通过@pytest.fixture来实现pytest前后置应用
1)定义fixture(不带参数):使用@pytest.fixture装饰器定义一个fixture函数,fixture函数可以包含初始等逻辑
python
import pytest
# 不带函数的fixture夹具
@pytest.fixture
def set_and_teardown():
print('测试函数执行前先执行前置操作')
class TestFixture:
def test_case_01(self):
print('这个是第一个测试用例')
def test_case_02(self):
print('这个是第二个测试用例')
def test_case_03(self):
print('这个是第三个测试用例')
if __name__ == '__main__':
pytest.main(['-k', 'test_setup_teardown'])
**2)在测试函数使用Fixture:**使用测试函数的参数来接收fixture的返回值,这样测试函数就可以在执行前后使用fixture提供的资源或状态。
python
import pytest
# 不带函数的fixture夹具
@pytest.fixture
def set_and_teardown():
print('测试函数执行前先执行前置操作')
class TestFixture:
def test_case_01(self, set_and_teardown):
print('这个是第一个测试用例')
def test_case_02(self):
print('这个是第二个测试用例')
def test_case_03(self, set_and_teardown):
print('这个是第三个测试用例')
if __name__ == '__main__':
pytest.main(['-k', 'test_fixture'])
3)执行顺序:pytest会自动检测测试函数中的参数,如果发现参数的名称与fixture的名称匹配,就会将fixture注入到测试函数中,执行顺序为先执行fixture的前置应用代码,然后再去执行测试函数,最后执行fixture的后置应用代码。
第一种方式:通过函数嵌套,将函数注册为最终器实现前后置应用
python
import pytest
# 不带函数的fixture夹具
@pytest.fixture
def set_and_teardown(request):
print('前置操作:初始化数据,清理上次运行测试结果!')
def teardown():
print('后置操作:清理测试数据!')
request.addfinalizer(teardown) # 将teardown函数注册为最终器,以确保在测试用例运行结束后执行后置操作
return teardown
class TestFixture:
def test_case_01(self, set_and_teardown):
print('这个是第一个测试用例')
def test_case_02(self):
print('这个是第二个测试用例')
def test_case_03(self, set_and_teardown):
print('这个是第三个测试用例')
if __name__ == '__main__':
pytest.main(['-k', 'test_fixture'])
第二种方式:通过yield关键词实现前后置应用
python
import pytest
# 不带函数的fixture夹具
@pytest.fixture
def set_and_teardown():
print('前置操作:初始化数据,清理上次运行测试结果!')
yield
print('后置操作:清理测试数据!')
class TestFixture:
def test_case_01(self, set_and_teardown):
print('这个是第一个测试用例')
def test_case_02(self):
print('这个是第二个测试用例')
def test_case_03(self, set_and_teardown):
print('这个是第三个测试用例')
if __name__ == '__main__':
pytest.main(['-k', 'test_fixture'])
4)定义fixture(带参数):使用@pytest.fixture装饰器定义一个fixture函数,可以携带参数:@pytest.fixture(scope="", autouse="", params="", ids="", name="")
- 1)scope:控制fixture的作用范围,可选值有:"function","class","session","module"
- function:默认(默认情况可以不写这个参数),测试函数级别,每个测试函数执行前后都会运行一次前后置
- class:控制单个文件每个类,类级别,在每个测试类执行前只运行一次前后置
- module:控制全局,每个模块(即每个.py)执行前后运行一次
- session:控制全局,整个pytest会话(多个模块)执行前后运行一次
- 2)autouse:用于指定是否自动应用fixture而无需在测试函数中显示的调用,默认为False
- 3)params:用于参数化的一个选项,它允许你为fixture定义多个参数值,以便在测试函数中使用不同的参数组合运行测试
- 4)ids:为参数化的fixture提供自定义的标识,用于在测试报告中更加清晰显示参数,需要跟params参数结合起来 使用
- 5)name:自定义fixture的名称
python
import pytest
@pytest.fixture(scope="function", autouse=True)
def set_and_teardown():
print('前置操作:初始化数据,清理上次运行测试结果!')
yield
print('后置操作:清理测试数据!')
@pytest.fixture(scope="function", autouse=True, params=['北京', '成都', '重庆'], ids=['BJ', 'CD', 'CQ'], name='setValue')
def set_params(request):
return request.param
class TestFixture:
def test_case_01(self, setValue):
print('这个是第一个测试用例')
print(f'获取到的值为:{setValue}')
def test_case_02(self):
print('这个是第二个测试用例')
def test_case_03(self):
print('这个是第三个测试用例')
if __name__ == '__main__':
pytest.main(['-k', 'test_fixture'])
9.3 使用@pytest.fixture和conftest.py结合使用实现一个全局的前后置应用
@pytest.fixture与conftest.py文件结合使用,可以实现在多个测试模块(.py)中共享前后置应用操作,这种集合的方式使得可以在整个测试项目中定义和维护通用的前后置逻辑,使测试代码更加模块化和可维护。
规则:
- conftest.py是一个单独存放的夹具配置文件,名称是固定写法不能更改
- 你可以在项目中的不同不同目录创建多个conftest.py,每个conftest.py文件都会对其所在的目录及其子目录下的测试模块生效
- 在不同模块的测试中需要用到conftest.py的前后置功能时,不需要在任何的导入操作,可以直接在测试函数的参数中使用fixture名称
- 作用:可以在不同的py文件中使用同一个fixture函数
python
import pytest
@pytest.fixture(scope="session",autouse=True)
def print_info():
print('-------------接口开始测试----------')
yield
print('-------------接口测试结束----------')