使用 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 ) username = f"user_{user_id} " return {"user_id" : user_id, "username" : username}
在上面的例子中,user_data
中的 user_id
和 username
是动态生成的,每次运行测试时其值都不同。
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 (): client = APIClient(base_url="http://example.com" ) yield client client.close()
动态生成的 HTTP 客户端 api_client
可以被每个测试用例独立复用。
5. 提供共享的动态数据 如果不使用 pytest.fixture
,你可能在不同测试函数中会对相同测试数据重复编码,而使用 fixture,则只需要定义一次逻辑,动态值可复用。
fixture 优势对比
fixture 对比定义常量优势,fixture 生成的数据每个用例调用时都重新生成了一遍,这样就可以支持在 fixture 修饰函数内部加入动态的逻辑,如 random.randint
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@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 ]}@pytest.mark.parametrize( "data_source_fixture" , [ "data_source_a" , "data_source_b" , "data_source_c" , ] )def test_dynamic_fixture (data_source_fixture, request ): 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' ]} " )
解释
基本用法 :
test_basic_parametrize
函数被参数化为三个测试用例,分别使用 (1, 2)
、(2, 3)
和 (3, 4)
作为参数。
每次运行测试函数时,sample_data
和 expected
会被赋值为参数列表中的一对值。
高级用法 :
data_source_a
、data_source_b
和 data_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
可以帮助更好地组织和管理数据,而 parametrize
和 request.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 dataclassimport pytest@dataclass class User : id : int username: str email: str @dataclass class TestCase : user_fixture: str expected_username: str expected_email: str @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 ): 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} " )
解释
定义数据类 : 使用 dataclass
定义 User
和 TestCase
数据类。User
类用于表示用户信息,TestCase
类用于表示测试用例,包含一个 fixture 名称和预期的用户名和电子邮件。
定义用户数据的 fixture : 创建多个 fixture,分别返回不同的 User
实例。
定义测试用例 : 创建一个包含多个 TestCase
实例的列表,每个实例包含一个 fixture 名称和相应的预期值。
参数化测试用例 : 使用 pytest.mark.parametrize
将 test_user
函数参数化,使其能够接收不同的 TestCase
实例。
动态获取 fixture 的值 : 在 test_user
函数中,使用 request.getfixturevalue(case.user_fixture)
动态获取对应的 fixture 实例,然后对其进行断言和操作。
request.param 和 indirect ,根据参数动态修改 fixture 值 在 pytest 中,request.param
和 indirect
参数用于参数化测试用例时提供更高级的功能和灵活性。
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 ): 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 ]
在这个示例中:
sample_data
fixture 使用 request.param
获取参数化的值,并根据该值生成测试数据。
pytest.mark.parametrize
对 sample_data
进行参数化,传递 [1, 2, 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 ): 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 ]
在这个示例中:
indirect=True
表示参数化的值将被传递给所有 fixture。
indirect={"sample_data": True}
表示参数化的值将被传递给特定的 fixture sample_data
。
更复杂的示例 下面是一个更复杂的示例,展示了如何结合使用 request.param
和 indirect
参数:
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 dataclassimport pytest@dataclass class User : id : int username: str email: str @pytest.fixture def user_data (request ): 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} " )
在这个示例中:
user_data
fixture 使用 request.param
获取参数化的值,并返回一个 User
对象。
pytest.mark.parametrize
对 user_data
进行参数化,传递 test_cases
作为参数。
indirect=True
表示参数化的值将被传递给 user_data
fixture,而不是直接传递给测试函数。
patch 使用,模拟替换注入函数行为 patch
作为 unittest.mock
模块的一部分,通常会与 pytest
一起使用。这是因为 pytest
是一个非常流行的测试框架,而 unittest.mock
提供了强大的模拟和替换功能,两者结合使用可以编写更强大和灵活的测试。
以下是一个完整的示例文件,展示了如何结合使用 patch
和 pytest
编写测试用例:
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 pytestfrom unittest.mock import patchdef is_path_exists (path ): import os return os.path.exists(path)@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' )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()
解释
定义被测试函数 :
is_path_exists
函数检查给定路径是否存在。
使用 patch
作为装饰器 :
@patch('os.path.exists', return_value=True)
替换 os.path.exists
方法,使其在测试期间总是返回 True
。
mock_exists
是模拟对象,在测试中用于验证 os.path.exists
的调用。
使用 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 .fixturedef dump_to_yaml_stream_file (request ): file_path = "./test.yaml" def remove_test_file (): if os.path.exists(file_path): os.remove(file_path) 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 定义测试类时,有一些规则和注意事项可以帮助确保测试用例能够被正确识别和执行。以下是一些关键点:
定义测试类的规则和注意事项
测试类名 :
测试类名应以 Test
开头,以便 pytest 能够自动识别它们。这是 pytest 的命名约定。
示例: class TestMyFeature:
测试方法名 :
测试方法名应以 test_
开头,以便 pytest 能够识别它们为测试方法。
示例: def test_example(self):
实例方法 :
测试方法应为实例方法,而不是类方法(即不使用 @classmethod
装饰器)。Pytest 期望测试方法是实例方法。
不要使用 @classmethod
装饰器。
Fixture 的使用 :
Fixture 可以在模块级别或类级别定义。模块级别的 fixture 可以在整个模块中重用,类级别的 fixture 仅在类内部可用。
使用 @pytest.fixture
装饰器定义 fixture。
如果需要在类内部定义 fixture,可以使用 self
作为第一个参数。
Fixture 的作用范围 :(不确定这个 scope 有效)
Fixture 的作用范围可以通过 scope
参数指定,如 "function"
(默认)、"class"
、"module"
或 "session"
。
示例: @pytest.fixture(scope="class")
资源清理 :
可以使用 request.addfinalizer
或 yield
关键字在 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 pytestimport osfrom unittest.mock import patch, MagicMockfrom etl.vector.convert.proto_2_vector import to_vector_config_clean, CleanRule, VectorConfigfrom etl.vector.manage.manage import dict_to_yaml_stream@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) 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 ruleclass 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)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()
解释
模块级别的 fixture :
dump_to_yaml_stream_file
和 preview_assign_data
是模块级别的 fixture,可以在整个模块中重用。
类内部定义 fixture :
clean_rule
fixture 定义在类内部,并使用 self
作为第一个参数,以便在类内部的测试用例中使用。
测试方法 :
test_dump_yaml
是一个实例方法(没有使用 @classmethod
装饰器),并且方法名以 test_
开头,符合 pytest 的命名约定。
类外部的测试用例 :
test_dump_yaml_outside_class
是一个类外部的测试用例,使用了模块级别的 fixture dump_to_yaml_stream_file
和 preview_assign_data
。
Pytest-mock pytest-mock