pytest测试用例编写

使用 pytest.fixture 生成用例数据

1. 动态生成不同的输入数据

尽管你的 user_data() 示例中返回的是静态数据,但 fixture 并不局限于返回静态值。你可以在 fixture 中用编程逻辑、随机生成器或数据库查询等方式生成测试所需的动态数据。例如:

1
2
3
4
5
6
7
8
import random

@pytest.fixture
def user_data():
# 动态生成用户数据
user_id = random.randint(1, 1000) # 动态生成随机用户 ID
username = f"user_{user_id}"
return {"user_id": user_id, "username": username}

在上面的例子中,user_data 中的 user_idusername 是动态生成的,每次运行测试时其值都不同。


2. 提供动态依赖:从外部数据源加载

pytest.fixture 可以动态地从文件、数据库或外部服务加载内容。这非常有用,尤其在复杂测试环境中。例如:

1
2
3
4
5
@pytest.fixture
def user_data_from_db():
# 从数据库动态读取用户信息
user = get_user_from_database(user_id=1) # 假设这是某个动态查询函数
return {"user_id": user.id, "username": user.name}

这里的 user_data_from_db 会根据数据库中的内容动态生成用户数据。


3. 支持参数化动态生成

pytest.fixture 是高度可扩展的,可以通过 request 对象实现 动态参数化生成。例如:

1
2
3
4
5
6
7
8
9
10
@pytest.fixture
def user_data(request):
# 根据测试需求动态生成不同的用户数据
user_id = request.param
return {"user_id": user_id, "username": f"user_{user_id}"}

@pytest.mark.parametrize("user_data", [1, 2, 3], indirect=True)
def test_user(user_data):
assert "user_id" in user_data
assert user_data["user_id"] in [1, 2, 3]

在这个例子里,user_data 中的值是由参数化动态生成的。


4. 提供上下文和多次初始化

Fixture 可以模拟「创建上下文-清理资源」的机制,比如打开网络连接、创建 HTTP 会话或初始化 mock 数据。在这种场景下,动态生成就十分重要:

1
2
3
4
5
6
@pytest.fixture
def api_client():
# 动态初始化 HTTP 客户端
client = APIClient(base_url="http://example.com") # 假设这是某个 HTTP 客户端类
yield client # 提供给测试用例使用
client.close() # 测试结束后自动释放资源

动态生成的 HTTP 客户端 api_client 可以被每个测试用例独立复用。


5. 提供共享的动态数据

如果不使用 pytest.fixture,你可能在不同测试函数中会对相同测试数据重复编码,而使用 fixture,则只需要定义一次逻辑,动态值可复用。

fixture 优势对比

  1. fixture 对比定义常量优势,fixture 生成的数据每个用例调用时都重新生成了一遍,这样就可以支持在 fixture 修饰函数内部加入动态的逻辑,如 random.randint
  2. fixture 对比定义函数优势,1. fixture 定义的函数后面使用时不需要加 () 2. fixture 支持上下文管理和多次初始化

Parametrize 参数化测试用例

在 pytest 中,pytest.mark.parametrize 可以用来参数化测试用例,使得同一个测试函数可以使用不同的输入参数运行多次。对于简单的常量参数,可以直接传递常量值。而对于复杂的场景,比如参数化的值是 fixture 的名称,则需要使用 request.getfixturevalue 来动态获取这些 fixture 的值。

name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import pytest

# 基本用法:参数化测试用例,使用常量值
@pytest.mark.parametrize("sample_data,expected", [
(1, 2),
(2, 3),
(3, 4),
])
def test_basic_parametrize(sample_data, expected):
assert sample_data + 1 == expected

# 定义多个数据源 Fixture
@pytest.fixture
def data_source_a():
return {"source": "A", "data": [1, 2, 3]}

@pytest.fixture
def data_source_b():
return {"source": "B", "data": [4, 5, 6]}

@pytest.fixture
def data_source_c():
return {"source": "C", "data": [7, 8, 9]}

# 高级用法:参数化测试用例,使用 fixture 名称
@pytest.mark.parametrize(
"data_source_fixture",
[
"data_source_a",
"data_source_b",
"data_source_c",
]
)
def test_dynamic_fixture(data_source_fixture, request):
# 使用 request.getfixturevalue 方法动态获取 fixture 的值
data_source = request.getfixturevalue(data_source_fixture)
assert "source" in data_source
assert "data" in data_source
print(f"Using data source: {data_source['source']} with data: {data_source['data']}")

解释

  1. 基本用法:

    • test_basic_parametrize 函数被参数化为三个测试用例,分别使用 (1, 2)(2, 3)(3, 4) 作为参数。
    • 每次运行测试函数时,sample_dataexpected 会被赋值为参数列表中的一对值。
  2. 高级用法:

    • data_source_adata_source_bdata_source_c 是三个不同的数据源 fixture。
    • test_dynamic_fixture 函数被参数化为三个测试用例,分别使用 "data_source_a""data_source_b""data_source_c" 作为参数。
    • 在测试函数中,使用 request.getfixturevalue(data_source_fixture) 动态获取对应的 fixture 实例,然后对其进行断言和操作。

Dataclass 使用

在使用 dataclass 的情况下,确实可能会出现使用 parametrize 用 fixture 名作为参数输入的情况,特别是在测试用例需要动态获取 fixture 的值时。这种情况下,dataclass 可以帮助更好地组织和管理数据,而 parametrizerequest.getfixturevalue 可以帮助动态获取和使用这些数据。

以下是一个完整的示例,展示了如何在使用 dataclass 的情况下,使用 parametrize 和 fixture 名作为参数输入的情况:

name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from dataclasses import dataclass
import pytest

# 定义数据类
@dataclass
class User:
id: int
username: str
email: str

@dataclass
class TestCase:
user_fixture: str
expected_username: str
expected_email: str

# 定义多个用户数据的 fixture
@pytest.fixture
def user_a():
return User(id=1, username="testuser1", email="test1@example.com")

@pytest.fixture
def user_b():
return User(id=2, username="testuser2", email="test2@example.com")

@pytest.fixture
def user_c():
return User(id=3, username="testuser3", email="test3@example.com")

# 定义测试用例
test_cases = [
TestCase(user_fixture="user_a", expected_username="testuser1", expected_email="test1@example.com"),
TestCase(user_fixture="user_b", expected_username="testuser2", expected_email="test2@example.com"),
TestCase(user_fixture="user_c", expected_username="testuser3", expected_email="test3@example.com"),
]

# 参数化测试用例
@pytest.mark.parametrize("case", test_cases)
def test_user(case, request):
# 动态获取 fixture 的值
user = request.getfixturevalue(case.user_fixture)

# 执行断言
assert user.username == case.expected_username
assert user.email == case.expected_email
print(f"Using user: {user.username} with email: {user.email}")

解释

  1. 定义数据类: 使用 dataclass 定义 UserTestCase 数据类。User 类用于表示用户信息,TestCase 类用于表示测试用例,包含一个 fixture 名称和预期的用户名和电子邮件。
  2. 定义用户数据的 fixture: 创建多个 fixture,分别返回不同的 User 实例。
  3. 定义测试用例: 创建一个包含多个 TestCase 实例的列表,每个实例包含一个 fixture 名称和相应的预期值。
  4. 参数化测试用例: 使用 pytest.mark.parametrizetest_user 函数参数化,使其能够接收不同的 TestCase 实例。
  5. 动态获取 fixture 的值: 在 test_user 函数中,使用 request.getfixturevalue(case.user_fixture) 动态获取对应的 fixture 实例,然后对其进行断言和操作。

request.param 和 indirect ,根据参数动态修改 fixture 值

在 pytest 中,request.paramindirect 参数用于参数化测试用例时提供更高级的功能和灵活性。

request.param

request.param 是 pytest 内置的 request fixture 的一个属性,用于在参数化 fixture 中获取当前参数的值。它通常与 pytest.mark.parametrize 结合使用,用于在 fixture 中动态生成测试数据。

示例

name
1
2
3
4
5
6
7
8
9
10
import pytest

@pytest.fixture
def sample_data(request):
# 使用 request.param 获取参数化的值
return request.param * 2

@pytest.mark.parametrize("sample_data", [1, 2, 3], indirect=True)
def test_sample_data(sample_data):
assert sample_data in [2, 4, 6]

在这个示例中:

  1. sample_data fixture 使用 request.param 获取参数化的值,并根据该值生成测试数据。
  2. pytest.mark.parametrizesample_data 进行参数化,传递 [1, 2, 3] 作为参数。
  3. indirect=True 表示参数化的值将被传递给 fixture,而不是直接传递给测试函数。

indirect 参数

indirect 参数用于告诉 pytest 参数化的值应传递给 fixture 而不是直接传递给测试函数。它可以是布尔值(对所有参数生效)或字典(对特定参数生效)。

示例

name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pytest

@pytest.fixture
def sample_data(request):
# 使用 request.param 获取参数化的值
return request.param * 2

@pytest.mark.parametrize("sample_data", [1, 2, 3], indirect=True)
def test_sample_data(sample_data):
assert sample_data in [2, 4, 6]

@pytest.mark.parametrize("sample_data", [1, 2, 3], indirect={"sample_data": True})
def test_sample_data_specific(sample_data):
assert sample_data in [2, 4, 6]

在这个示例中:

  1. indirect=True 表示参数化的值将被传递给所有 fixture。
  2. indirect={"sample_data": True} 表示参数化的值将被传递给特定的 fixture sample_data

更复杂的示例

下面是一个更复杂的示例,展示了如何结合使用 request.paramindirect 参数:

name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from dataclasses import dataclass
import pytest

@dataclass
class User:
id: int
username: str
email: str

@pytest.fixture
def user_data(request):
# 使用 request.param 获取参数化的值,并返回 User 对象
user_info = request.param
return User(id=user_info['id'], username=user_info['username'], email=user_info['email'])

# 定义测试用例
test_cases = [
{"id": 1, "username": "testuser1", "email": "test1@example.com"},
{"id": 2, "username": "testuser2", "email": "test2@example.com"},
{"id": 3, "username": "testuser3", "email": "test3@example.com"},
]

# 参数化测试用例
@pytest.mark.parametrize("user_data", test_cases, indirect=True)
def test_user_data(user_data):
assert user_data.username.startswith("testuser")
assert user_data.email.endswith("@example.com")
print(f"Using user: {user_data.username} with email: {user_data.email}")

在这个示例中:

  1. user_data fixture 使用 request.param 获取参数化的值,并返回一个 User 对象。
  2. pytest.mark.parametrizeuser_data 进行参数化,传递 test_cases 作为参数。
  3. indirect=True 表示参数化的值将被传递给 user_data fixture,而不是直接传递给测试函数。

patch 使用,模拟替换注入函数行为

patch 作为 unittest.mock 模块的一部分,通常会与 pytest 一起使用。这是因为 pytest 是一个非常流行的测试框架,而 unittest.mock 提供了强大的模拟和替换功能,两者结合使用可以编写更强大和灵活的测试。

以下是一个完整的示例文件,展示了如何结合使用 patchpytest 编写测试用例:

name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pytest
from unittest.mock import patch

# 假设这是一个我们要测试的函数
def is_path_exists(path):
import os
return os.path.exists(path)

# 使用 patch 作为装饰器
@patch('os.path.exists', return_value=True)
def test_is_path_exists(mock_exists):
result = is_path_exists('some_path')
assert result is True
mock_exists.assert_called_once_with('some_path')

# 使用 patch 作为上下文管理器
def test_is_path_exists_with_context():
with patch('os.path.exists', return_value=True) as mock_exists:
result = is_path_exists('some_path')
assert result is True
mock_exists.assert_called_once_with('some_path')

if __name__ == '__main__':
pytest.main()

解释

  1. 定义被测试函数:

    • is_path_exists 函数检查给定路径是否存在。
  2. 使用 patch 作为装饰器:

    • @patch('os.path.exists', return_value=True) 替换 os.path.exists 方法,使其在测试期间总是返回 True
    • mock_exists 是模拟对象,在测试中用于验证 os.path.exists 的调用。
  3. 使用 patch 作为上下文管理器:

    • with patch('os.path.exists', return_value=True) as mock_exists: 在上下文管理器中替换 os.path.exists 方法,使其在测试期间总是返回 True
    • mock_exists 同样是模拟对象,用于验证 os.path.exists 的调用。

request.addfinalizer

request.addfinalizer 用于在调用的测试用例执行完毕后执行清理操作。适用于需要在特定测试用例结束后进行资源清理的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@pytest.fixture
def dump_to_yaml_stream_file(request):
file_path = "./test.yaml"

def remove_test_file():
if os.path.exists(file_path):
os.remove(file_path)

# 添加清理操作到 finalizer
request.addfinalizer(remove_test_file)
return file_path

def test_dump_yaml(dump_to_yaml_stream_clean_rule, dump_to_yaml_stream_file):
dict_to_yaml_stream(
dump_to_yaml_stream_clean_rule.dict(), file_path=dump_to_yaml_stream_file, multiline_keys=["source"]
)

定义 Test 类

在使用 pytest 定义测试类时,有一些规则和注意事项可以帮助确保测试用例能够被正确识别和执行。以下是一些关键点:

定义测试类的规则和注意事项

  1. 测试类名:

    • 测试类名应以 Test 开头,以便 pytest 能够自动识别它们。这是 pytest 的命名约定。
    • 示例: class TestMyFeature:
  2. 测试方法名:

    • 测试方法名应以 test_ 开头,以便 pytest 能够识别它们为测试方法。
    • 示例: def test_example(self):
  3. 实例方法:

    • 测试方法应为实例方法,而不是类方法(即不使用 @classmethod 装饰器)。Pytest 期望测试方法是实例方法。
    • 不要使用 @classmethod 装饰器。
  4. Fixture 的使用:

    • Fixture 可以在模块级别或类级别定义。模块级别的 fixture 可以在整个模块中重用,类级别的 fixture 仅在类内部可用。
    • 使用 @pytest.fixture 装饰器定义 fixture。
    • 如果需要在类内部定义 fixture,可以使用 self 作为第一个参数。
  5. Fixture 的作用范围:(不确定这个 scope 有效)

    • Fixture 的作用范围可以通过 scope 参数指定,如 "function"(默认)、"class""module""session"
    • 示例: @pytest.fixture(scope="class")
  6. 资源清理:

    • 可以使用 request.addfinalizeryield 关键字在 fixture 中定义清理操作。
    • 示例: 使用 request.addfinalizer 添加清理操作。

示例文件

以下是一个完整的示例文件,展示了如何定义一个测试类及其 fixture,并遵循上述规则和注意事项:

name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import pytest
import os
from unittest.mock import patch, MagicMock
from etl.vector.convert.proto_2_vector import to_vector_config_clean, CleanRule, VectorConfig
from etl.vector.manage.manage import dict_to_yaml_stream

# 模块级别的 fixture
@pytest.fixture
def dump_to_yaml_stream_file(request):
file_path = "./test.yaml"

def remove_test_file():
if os.path.exists(file_path):
os.remove(file_path)

# 添加清理操作到 finalizer
request.addfinalizer(remove_test_file)
return file_path

@pytest.fixture(scope="class")
def preview_assign_data():
rule = MagicMock(spec=CleanRule)
rule.data_sources = ['source1']
rule.data_sinks = ['sink1']
return rule

# 定义测试类
class TestDumpToYamlStream:
@pytest.fixture(scope="class")
def clean_rule(self, preview_assign_data):
return to_vector_config_clean(preview_assign_data)

@patch('etl.vector.manage.manage.dict_to_yaml_stream', return_value=True)
def test_dump_yaml(self, mock_dict_to_yaml_stream, clean_rule, dump_to_yaml_stream_file):
dict_to_yaml_stream(
clean_rule.dict(), file_path=dump_to_yaml_stream_file, multiline_keys=["source"]
)
assert os.path.exists(dump_to_yaml_stream_file)

# 类外部使用模块级别的 fixture 的测试用例
def test_dump_yaml_outside_class(dump_to_yaml_stream_file, preview_assign_data):
clean_rule = to_vector_config_clean(preview_assign_data)
dict_to_yaml_stream(
clean_rule.dict(), file_path=dump_to_yaml_stream_file, multiline_keys=["source"]
)
assert os.path.exists(dump_to_yaml_stream_file)

if __name__ == '__main__':
pytest.main()

解释

  1. 模块级别的 fixture:

    • dump_to_yaml_stream_filepreview_assign_data 是模块级别的 fixture,可以在整个模块中重用。
  2. 类内部定义 fixture:

    • clean_rule fixture 定义在类内部,并使用 self 作为第一个参数,以便在类内部的测试用例中使用。
  3. 测试方法:

    • test_dump_yaml 是一个实例方法(没有使用 @classmethod 装饰器),并且方法名以 test_ 开头,符合 pytest 的命名约定。
  4. 类外部的测试用例:

    • test_dump_yaml_outside_class 是一个类外部的测试用例,使用了模块级别的 fixture dump_to_yaml_stream_filepreview_assign_data

Pytest-mock

pytest-mock


pytest测试用例编写
https://abrance.github.io/2025/04/03/mdstorage/domain/python/pytest测试用例编写/
Author
xiaoy
Posted on
April 3, 2025
Licensed under