目录
[yield fixture](#yield fixture)
介绍
pytest官方文档:https://docs.pytest.org/en/stable/getting-started.html
pytest是一个非常流行的Python测试框架,它提供了丰富的功能和灵活的用法,使得编写和运行测试用例变的简单而高效
优势:
- 简单易用: pytest 的语法简洁清晰,对于编写测试用例非常友好,几乎可以在几分钟内上手。
- 强大的断言库: pytest 内置了丰富的断言库,可以轻松地进行测试结果的判断。
- 支持参数化测试: pytest 支持参数化测试,允许使用不同的参数多次运行同一个测试函数,这大提高了测试效率。
- 丰富的插件生态系统: pytest 有着丰富的插件生态系统,可以通过插件扩展各种功能,比如覆盖率测试、测试报告生成(如 pytest-html 插件可以生成完美的HTML测试报告)、失败用例重复执行(如 pytest-rerunfailures 插件)等。此外, pytest 还支持与selenium、requests、appinum等结合,实现Web自动化、接口自动化、App自动化测试。
- 灵活的测试控制: pytest 允许跳过指定用例,或对某些预期失败的case标记成失败,并支持重复执行失败的case。
安装:
pip install pytest
下面是不同的pytest版本支持的python版本:
| pytest版本 | 最低Python版本 |
|---|---|
| 8.0+ | 3.8+ |
| 7.1+ | 3.7+ |
| 6.2 - 7.0 | 3.6+ |
| 5.0 - 6.1 | 3.5+ |
| 3.3 - 4.6 | 2.7,3.4+ |
安装成功后,有的方法名前面有直接运行标志

但是并不是所有的方法都可以直接运行,需要遵循pytest中用例命名规则
用例运行规则

1、文件名必须以 test_ 开头或者 _test 结尾
2、测试类必须以 Test 开头,并且不能有 init 方法
3、测试方法必须以 test 开头
当满足以上后,可通过命令行参数pytest直接运行符合条件的用例:

注意:Python类中不可以添加init方法
class Test(): def __init__(self): print("-----init-------") def test_a(self): print("-----test_a----")
执行结果

由于pytest的测试收集机制,测试类中不可以定义__init__方法。pytest采用自动发现机制来收集测试用例。它会自动实例化测试类并调用其所有以test结尾的方法作为测试用例。如果测试类中定义了__init__方法,那么当pytest实例化该类时,__init__方法会被调用,这可能会掩盖测试类的实际测试逻辑,并引入额外的副作用,影响测试结果的准确性。
测试类中存在初始化操作该采取什么方案?
为了避免使用__init__方法,建议在pytest中使用其他替代方案,如使用setUp()和tearDown()方法、使用类属性、使用fixture函数
pytest命令参数
pytest 提供了丰富的命令行选择来控制测试的执行。以下是一些常用的pytest命令行参数及其适用说明
| 命令 | 描述 | 备注 |
|---|---|---|
| pytest | 在当前目录及其子目录中搜索并运行测试 | |
| pytest -v | 增加输出的详细程度 | |
| pytest -s | 显示测试中的 print 语句 | |
| pytest test_module.py | 运行指定的测试模块 | |
| pytest test_dir/ | 运行指定目录下的所有测试 | |
| pytest -k <keyword> | 只运行测试名包含指定关键字的测试 | |
| pytest -m <marker> | 只运行标记为指定标记的测试 | |
| pytest -q | 减少输出的详细程度 | |
| pytest --html=report.html | 生成HTML格式的测试报告 | 需要安装pytest-html插件 |
| pytest --cov | 测量测试覆盖率 | 需要安装pytest-cov插件 |
当我们既要详细输出,又要指定文件时,命令很长,并且每次运行都需要手动输入命名时,我们可以将需要的相关配置参数统一放到pytest配置文件中。
pytest配置文件
在当前项目下创建pytest.ini文件,该文件为pytest的配置文件,以下为常见的配置选项:
| 参数 | 解释 |
|---|---|
| addopts | 指定在命令行中默认包含的选择 |
| testpaths | 指定搜索测试的目录 |
| python_files | 指定发现测试模块时使用的文件匹配模式 |
| python_classes | 指定发现测试类时使用的类名前缀或模式 |
| python_functions | 指定发现测试函数和方法时使用的函数前缀或模式 |
| norecursedirs | 指定在搜索测试时应该避免递归进入的目录模式 |
| markers | 定义测试标记,用于标记测试用例 |
示例:详细输出cases包下文件名以test_开头且方法名以test开头的所有用例
[pytest] addopts = -vs testpaths = ./cases python_files = test_*.py python_classes = test*
配置好pytest.ini文件后,命令行执行pytest命令即可,无需再额外指定其他参数

pytest.ini 文件通常位于项目的根目录下。通过在pytest.ini中定义配置项,可以覆盖pytest的默认行为,以满足项目的需求
前后置
上面我们说到,在使用pytest框架中,测试类中不可以添加init()方法,如何进行数据的初始化?
在测试框架中,前后置是指在执行测试用例前和测试用例后执行一些额外的操作,这些操作可以用于设置测试环境、准备测试数据等,以确保测试的可靠性
pytest 框架提供三种方法做前后置的操作:
- setup_method 和 teardown_method :这两个方法用于类中的每个测试方法的前置和后置操作
- setup_class 和 teardown_class : 这两个方法用于整个测试类的前置和后置操作
- fixture: 这是pytest推荐的方式来实现测试用例的前置和后置操作。fixture提供了更灵活的控制和更强大的功能。
示例
python
import pytest
class TestExample:
def setup_method(self):
print("Setup: Before each test")
def teardown_method(self):
print("Teardown: After each test")
def test_example1(self):
print("Running test_example1")
def test_example2(self):
print("Running test_example2")

断言
断言(assert)是一种调试辅助工具,用于检查程序的状态是否符合预期。如果断言失败(即条件为假),python解释器将抛出一个AssertionError异常。断言通常用于检测程序中的逻辑错误。
pytest 允许你在Python测试中使用标准的Python assert 语句来验证预期和值。
基本语法:
assert 条件, 错误信息
条件:必须是一个布尔表达式
错误信息:当条件为假时显示的错误信息,可选。
示例1:基本数据类型的断言
python
#断言整数
a = 1
b = 2
assert a==b
#断言字符串
str = "hello"
assert "hello" == str
示例2:数据结构断言
python
def test():
#断言列表
expect_list = [1, 'apple', 3.14]
actual_list = [1, 'apple', 3.14]
#断言元组
expect_tuple = (1, 'apple', 3.14)
actual_tuple = (1, 'apple', 3.14)
#断言字典
expect_dict = {'name' : 'Alice','age' : 25}
actual_dict = {'name' : 'Alice','age' : 25}
#断言集合
expect_set = {1, 2, 3, 'apple'}
actual_set = {1, 2, 3, 'apple'}
assert expect_list == actual_list
assert expect_tuple == actual_tuple
assert expect_dict == actual_dict
assert expect_set == actual_set
示例3:函数断言
python
#函数断言
def divide(a,b):
assert b != 0
return a/b
#正常情况
print(divide(10,2))
#触发断言
print(divide(10,0)) # 抛出 AssertionError: 除数不能为0
示例4:接口返回值断言
python
#断言接口返回值完整字段和值
def test1():
url = "http://jsonplaceholder.typicode.com/posts/1"
r = requests.get(url=url)
expect_data = {
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
print(r.json())
assert r.json() == expect_data
assert r.json()['userId'] == 1
#断言接口返回值重要字段
def test2():
url = "http://jsonplaceholder.typicode.com/comments?postId=1"
r = requests.get(url=url)
print(r.json())
assert r.json()[1]['id'] == 1
#断言接口html返回值
def test3():
url = "http://jsonplaceholder.typicode.com/"
r = requests.get(url=url)
assert "Use your own data" in r.text
参数化
参数化设计是自动化设计中的一个重要组成部分,它通过定义设计参数和规则,使得设计过程更加灵活和可控
pytest中内置的pytest.mark.parametrize装饰器允许对测试函数的参数进行参数化
示例1:在用例上使用参数化
python
@pytest.mark.parametrize("test_input,expected",[("3+5",8),("2+4",6),
("6*9",42)])
def test_eval(test_input,expected):
assert eval(test_input) == expected

这里,@parametrize装饰器定义了三个不同的(test_input,expected)元组,以便test_eval函数将依次使用它们运行三次
也可以在类或模块上使用 parametrize 标记,这将使用参数集调用多个函数
参数2:在类上使用参数化
python
@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
要对模块中的所有测试进行参数化,你可以将pytestmark全局变量赋值:
python
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
除了使用@parametrize添加参数化外,pytest.fixture()允许对fixture函数进行参数化
示例3:自定义参数化数据源
python
def data_provider():
return ["a","b"]
#定义一个测试函数,它依赖于上面函数的返回值
@pytest.mark.parametrize("data",data_provider())
def test_data(data):
assert data != None
print(f"Testing with data provider: {data}")
fixture
pytest中的fixture是一种强大的机制,用于提供测试函数所需的资源或上下文。它可以用于设置测试环境、准备数据等。
基本使用
未使用fixture方法的调用
python
def fixture_01():
print("第⼀个fixture标记的⽅法")
def test_01():
fixture_01()
print("第⼀个测试⽤例")

fixture标记的方法调用
python
@pytest.fixture
def fixture_01():
print("第⼀个fixture标记的⽅法")
def test_01(fixture_01):
print("第⼀个测试⽤例")

未标记 fixture 方法的调用与 fixture 标记的方法调用完全不一样,前者需要在方法体中调用,而后者可以将函数名作为参数进行调用
测试脚本中存在的很多重复代码、公共的数据对象时,使用 fixture 最为合适
示例2:访问列表页和详情页之前都需要执行登陆操作
python
@pytest.fixture
def login():
print("---执行登陆操作---")
def test_list(login):
print("---访问列表页")
def test_detail(login):
print("---访问详情页")

通过使用 @pytest.fixture 装饰器来告诉 pytest 一个特定函数是一个 fixture,通过运行结果可见,在执行列表页和详情页之前都会执行 login 方法
fixture嵌套
python
@pytest.fixture
def first_entry():
return "a"
#安排
@pytest.fixture
def order(first_entry):
return [first_entry]
def test_string(order):
#行动
order.append("b")
#断言
assert order == ["a","b"]
测试不必局限于单个 fixture ,它们可以依赖于您想要的任意数量的 fixture,并且 fixture 也可以使用其他 fixture 。pytest 最伟大的优势之一是极其灵活的 fixture 系统,它允许我们将测试的复杂需求简化为更简单和有组织的函数,我们只需要每个函数描述它们所依赖的事物
请求多个fixture
python
class Fruit:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
@pytest.fixture
def my_fruit():
return Fruit("apple")
@pytest.fixture
def fruit_basket(my_fruit):
return [Fruit("banana"),my_fruit]
def test_my_friut_in_basket(my_fruit, fruit_basket):
assert my_fruit in fruit_basket
测试和 fixture 不仅限于一次请求单个 fixture ,它们可以请求任意多个
yield fixture
当我们运行测试时,我们希望确保它们能够自我清理,以便它们不会干扰其他测试(同时也避免留下大量测试数据来膨胀系统)。pytest中的 fixture 提供了一个非常有用拆卸系统,它允许我们为每个 fixture 定义具体的清理步骤
"Yield" fixture 使用 yield 而不是 return 。有了这些fixture,我们可以运行一些代码,并将对象返回给请求的 fixture/test,就像其他 fixture 一样,唯一不同是:
- return 被替换为 yield
- 该 fixture 的任何拆卸代码放置在 yield 之后
一旦 pytest 确定了 fixture 的线性顺序,它将运行每个 fixture 直到它返回或 yeild,然后继续执行列表中的下一个 fixture 做同样的事情
测试完成后,pytest 将逆向遍历 fixture 列表,对于每个 yield 的 fixture,运行 yield 语句之后的代码
示例1:
python
@pytest.fixture
def open_close():
print("前置操作,初始化...")
yield
print("后置操作,清理数据...")
def test_01(open_close):
print("第一个测试用例")

示例2:创建文件句柄与关闭文件
python
@pytest.fixture
def file_read():
print("打开文件句柄")
fo = open("test.txt","r")
yield fo
print("关闭打开的文件")
fo.close()
# @pytest.fixture
def file_write():
print("打开文件句柄")
fo = open("test.txt","w",encoding="utf-8")
return fo
# yield fo
# print("关闭文件句柄")
# fo.close()
def test_file(file_write, file_read):
#写入数据
w = file_write
w.write("测试数据")
w.close() #写入后关闭文件句柄,以便读取
#读取数据
r = file_read
str = r.read(10)
print("文件内容:",str)

带参数的fixture
pytest.fixture(scope='', params='', autouse='', ids='', name='')
参数详解:
- scope 参数用于控制 fixture 的作用范围,决定了fixture的生命周期。可选值有:
- function(默认):每个测试函数都会调用一次fixture
- class:在同一个测试类中共享这个fixture
- module:在同一个测试模块中共享这个fixture(一个文件里)
- session:整个测试会话中共享这个fixture
- autouse 参数默认为 False。如果设置为True,则每个测试函数都会自动调用该fixture。无需显式传入
- params 参数用于参数化fixture,支持列表传入。每个参数值都会使fixture执行一次,类似于for循环
- ids 参数与params配合使用,为每个参数化实例指定可读的标识符(给参数取名字)
- name 参数用于为 fixture 显式设置一个名称。如果使用了name,则在测试函数中需要使用这个名称来引用 fixture(给fixture取名字)
示例1:scope的使用
scope="function"
python
@pytest.fixture(scope="function")
def fixture_01():
print("初始化")
yield
print("清理")
class TestCase():
def test_01(self, fixture_01):
print("第⼀个测试⽤例")
def test_02(self, fixture_01):
print("第⼆个测试⽤例")

scope="class"
python
@pytest.fixture(scope="class")
def fixture_01():
print("初始化")
yield
print("清理")
class TestCase():
def test_01(self,fixture_01):
print("第一个测试用例")
def test_02(self,fixture_02):
print("第二个测试用例")

结论:
- scope 默认为 function ,这里的 function 可以省略不写,当 scope="function" 时, 每个测试函数都会调用一次 fixture 。 scope="class" 时,在同一个测试类中, fixture 只会在类中的第一个测试函数开始前执行一次,并在类中的最后一个测试函数结束后执行清理。
- 当scope="moudle"、scope="session"时可用于实现全局的前后置应用,这里需要多个文件配合
conftest.py 和 @pytest.fixture 结合使用实现全局的前后置应用
@pytest.fixture 与 conftest.py 文件结合使用,可以实现在多个测试模块( .py )文件中共享前后置操作,这种结合的方式使得可以在整个测试项目中定义和维护通用的前后置逻辑,使测试代码更加模块化和可维护。
规则:
- conftest.py 是一个单独存放的夹具配置文件,名称是固定的不能修改
- 你可以在项目中的不同目录下创建多个 contest.py 文件,每个 conftest.py 文件都会对其所在目录及其子目录下的测试模块生效
- 在不同模块的测试中需要用到 conftest.py 的前后置功能时,不需要做任何的import导入操作
- 作用:可以在不同的 .py 文件中使用同一个 fixture 函数
示例2: scope="moudle" 、 scope="session" 实现全局的前后置应用
项目结构;

python
@pytest.fixture(scope="module", autouse=True)
def fixture_01():
print("初始化")
yield
print("清理")
test_case_01.py
python
def test_case01():
print("单独放出来的测试⽤例01")
class TestCase01():
def test_01(self):
print("第⼀个测试⽤例")
def test_02(self):
print("第⼆个测试⽤例")
test_case_02.py
python
def test_case02():
print("单独放出来的测试⽤例02")
class TestCase02():
def test_01(self):
print("第⼀个测试⽤例")
def test_02(self):
print("第⼆个测试⽤例")
运行结果:

当 scope="session" 时:
test_case_01.py
python
def test_case01():
print("单独放出来的测试⽤例01")
class TestCase01():
def test_01(self):
print("第⼀个测试⽤例")
def test_02(self):
print("第⼆个测试⽤例")
test_case_02.py
python
def test_case02():
print("单独放出来的测试⽤例02")
class TestCase02():
def test_01(self):
print("第⼀个测试⽤例")
def test_02(self):
print("第⼆个测试⽤例")
运行结果:

示例3:autouse使用
python
@pytest.fixture(scope="class", autouse=True)
def fixture_01():
print("初始化")
yield
print("清理")
class TestCase():
def test_01(self):
print("第⼀个测试⽤例")
def test_02(self):
print("第⼆个测试⽤例")
运行结果:

autouse 默认为 False ,即当前的 fixture 需要手动显示调用,在该案例之前我们默认使用的都是 autouse=False
当 autouse=True 时, fixture 会在所有测试函数执行之前自动调用,无论这些测试函数是否显式地引用了该 fixture
示例4:通过params实现参数化
python
# 定义⼀个参数化的 fixture
@pytest.fixture(params=["a", "b"])
def data_provider(request):
return request.param
# 定义⼀个测试函数,它依赖于上⾯的参数化 fixture
def test_data(data_provider):
assert data_provider != None
print(f"Testing with data provider: {data_provider}")
前⾯我们已经学过pytest中通过 @pytest.mark.parametrize 实现参数化,通过 fixture 也 可以实现参数化,那么到底哪⼀种更好呢?
如果测试场景主要涉及简单的参数传递,且不需要复杂的资源管理,建议使用 parametrize,因为它更简单直接;如果测试需要动态加载外部数据,或者需要管理复杂的测试资源(如数据库连接、文件操作等),建议使用 fixture,在某些情况下,也可以结合使用 parametrize 和 fixture,以充分利用两者的优点。
总结来说,parametrize 更适合简单场景,而 fixture 更适合需要动态数据和资源管理的复杂场景。