[Python] 就是这样!pytest mocks 的总结!

您好!
我是系统开发部的福井!

由于我写这篇博客的年份是 2025 年,也就是中国十二生肖中的蛇年,
Python 测试框架 pytest想向大家介绍

虽然标题是“模拟总结”,但内容仅限于作者根据自己的个人经验和偏见选择的功能和用途,并且他认为这些功能和用途是“必须了解的”。

所以,我希望这篇文章能对那些还没有使用过pytest或者将来计划使用它的人有所帮助。

介绍

[介绍]

作为前提条件,本博客将主要 pytest-mock 代码
标准 Python 库中的 unittest.mock 请注意,其语法与
本博客 unittest.mock ;更多信息,请参阅以下 Python 官方文档:
https://docs.python.org/3/library/unittest.mock.html

【对于尚未使用过 pytest 的用户】

下面我还会写一些示例测试代码,但如果您以前没有使用过 pytest,我希望您能带着以下内容阅读它。

以下是一个在测试方法中实现的模拟示例,但`mocker`参数由 pytest-mock 提供的 fixture
因此,如果安装了 pytest-mock,则无需导入任何内容即可使用它。

def test_get_name(mocker): mocker.patch.object( (省略) )

本博客并未解释“什么是 fixture?”或“什么是 mocker?”(关于 mocker 的实际工作原理)。如果您对此感兴趣,请参阅 pytest 和 pytest-mock 的官方文档。https
://docs.pytest.org/en/stable/
https://pypi.org/project/pytest-mock/

模拟方法和属性

现在,让我们直入主题。
首先,我将解释如何模拟方法和属性。

模拟实例方法

首先,我们来看模拟实例方法,这在很多情况下都会用到。
例如,如果你有一个名为 UserInfo 的类,它有一个名为 get_name() 的方法,该方法返回一个 str 类型的值,那么一个示例实现如下所示。

def test_get_name(mocker): test_name = "测试名称" # 模拟 UserInfo 类的 get_name() 方法 mocker.patch.object( UserInfo, "get_name", return_value=test_name )

目标类对象或方法您可以使用第一个和第二个参数指定
如果您要指定返回值, `return_value` 的值则可以指定

模拟特定模块中的方法

如果要模拟类中未定义的方法,也可以通过指定模块来模拟它,如下所示。

def test_get_name(mocker): test_name = "测试名称" # 模拟 app.services.user 模块中的 get_name() 函数 mocker.patch( "app.services.user.get_name", return_value=test_name )

目标模块以及该模块中要执行的方法(以字符串形式)来模拟模块
实现此功能时,请务必正确指定模块。

通过多次执行不同的返回值来模拟同一个方法

有些情况下,被测方法可能会多次执行同一个方法。
如果可以接受固定的返回值,则可以使用上述方法进行测试。但是,根据流程的不同,执行路径可能会因返回值而改变。

这时 `side_effect` 就派上用场了。下面是一个实现示例。

def test_get_name(mocker): test_name_1 = "Ichiro Tanaka" test_name_2 = "Jiro Suzuki" # 模拟 UserInfo 类的 get_name() 方法 mocker.patch.object( UserInfo, 'get_name', side_effect=[ test_name_1, # 第一次执行时的返回值 test_name_2, # 第二次执行时的返回值 ] )

side_effect` 简而言之,`
回到主题,如果您想为每次执行次数指定不同的值,按照您希望的顺序,将值以列表的形式返回,可以
`side_effect` ,它是一个用途广泛的便捷功能。

模拟属性

如果想要模拟方法和属性,PropertyMock 可以使用
例如,如果 UserInfo 类有一个名为 first_name 的属性用于存储姓名,则实现方式如下:

def test_first_name(mocker): first_name_taro = "Taro" # 模拟 UserInfo 的 first_name 属性 mocker.patch.object( UserInfo, "first_name", new_callable=PropertyMock, return_value=first_name_taro )

PropertyMock 本身是 Python 标准库 unittest.mock 提供的一个类,用于模拟特定类的属性,就像本例中一样。(它与用于常规模拟的 Mock 类不同。)
代码本身与模拟方法时并没有太大区别,但指定 new_callable,例如 new_callable=PropertyMock请注意,您需要

执行模拟验证

到目前为止,我们已经讨论了如何模拟方法和属性。然而,
在实际实现单元测试时,模拟的进程是否按预期被调用 您可能需要验证
接下来,模拟方法的执行情况验证 我们将讨论如何

确认该操作已执行

首先,我们来看看如何验证一个方法是否被执行过。
它是否被调用过一次如果您只想验证`assert_called_once()` 可以使用
下面是一个示例实现

def test_get_name(mocker): test_name = "测试名称" # 模拟 UserInfo 类的 get_name() 方法 mock_get_name = mocker.patch.object( UserInfo, "get_name", return_value=test_name ) (※方法执行处理等) # 模拟的 get_name() 方法只执行了一次 mock_get_name.assert_called_once()

,假设它会被执行多次如果您想验证一个函数被调用的次数
,如下所示 assertcall_count 您可以使用

def test_get_name(mocker): test_name = "测试名称" # 模拟 UserInfo 类的 get_name() 方法 mock_get_name = mocker.patch.object( UserInfo, "get_name", return_value=test_name ) (※方法执行处理等) # get_name() 的模拟执行了两次 assert mock_get_name.call_count == 2

 

验证是否使用预期参数执行了该操作

接下来传递给模拟方法的参数是否符合预期我们将讨论如何验证
`assert_called_with()` 来实现
下面是一个示例实现。

def test_get_name(mocker): test_name = "测试名称" # 传递给模拟方法的参数的预期值 expected_user_id = 123 # 模拟 UserInfo 类的 get_name() 方法 mock_get_name = mocker.patch.object( UserInfo, "get_name", return_value=test_name ) (※方法执行处理等) # 传递给 get_name() 的参数是预期值 (expected_user_id) mock_get_name.assert_called_with(user_id=expected_user_id)

同时验证“是否已执行”和“参数”。

到目前为止,我们已经分别展示了如何验证“函数是否执行”和“参数是否符合预期”,但其实还可以同时验证它们。
`assert_called_once_with()` 方法,无需编写单独的代码即可执行这些验证。
下面展示了一个实现示例。

def test_get_name(mocker): test_name = "测试名称" # 传递给模拟方法的参数的预期值 expected_user_id = 123 # 模拟 UserInfo 类的 get_name() 方法 mock_get_name = mocker.patch.object( UserInfo, "get_name", return_value=test_name ) (※方法执行处理等) # 传递给 get_name() 的参数是预期值 (expected_user_id),并且已经执行过一次 mock_get_name.assert_called_once_with(user_id=expected_user_id)

如果您希望模拟方法只被调用一次,`assert_called_once_with()` 而不是 assert_called_with()`, 最好使用

模拟方法会被多次调用即使`assert_has_calls()` 也可以使用
下面提供了一个实现示例。

def test_get_name(mocker): test_name_1 = "Ichiro Tanaka" test_name_2 = "Jiro Suzuki" # 传递给模拟方法的预期参数值 expected_user_id_tanaka = 1 expected_user_id_suzuki = 2 # 模拟 UserInfo 类的 get_name() 方法 mock_get_name = mocker.patch.object( UserInfo, 'get_name', side_effect=[ test_name_1, # 第一次执行时的返回值 test_name_2, # 第二次执行时的返回值 ] ) (※方法执行处理等) # 确保 get_name() 方法按预期参数顺序执行 mock_get_name.assert_has_calls([ mocker.call(expected_user_id_tanaka), mocker.call(expected_user_id_suzuki) ])

获取传递给模拟方法的参数

此外,虽然它并非像上面描述的那样是一种验证方法,但检索传递给模拟方法的参数您也可以
模拟对象的 call_args.args` 来实现
检索到的值是 `tuple` 类型,可以作为位置参数获取。
下面提供了一个实现示例。

def test_get_name(mocker): test_name = "测试名称" # 模拟 UserInfo 类的 get_name() 方法 mock_get_name = mocker.patch.object( UserInfo, "get_name", return_value=test_name ) (※ 方法执行处理等) # 获取传递给模拟方法的参数 args = mock_get_name.call_args.args # 第一个参数 args_1 = args[0] # 第二个参数 args_2 = args[1]

如果你能获取到参数,就可以使用 assert 来验证它们(虽然你可能不一定需要使用 assert),而且
你还可以使用获取到的参数编写其他程序,所以我个人认为值得记住,并在这里介绍一下。
但是请,call_args 只能获取上次执行的参数,所以如果你想获取多次执行的方法的参数,应该使用 call_args_list

使用 side_effect 引发异常

最后,我们来看看如何使用模拟对象抛出异常。
,我们之前在另一个主题中也讨论过它 `side_effect` 我们将使用
下面是一个示例实现。

def test_get_name(mocker): test_error_message = "目标用户不存在" # 使用 UserInfo 类的 get_name() 方法抛出异常 mock_get_name = mocker.patch.object( UserInfo, "get_name", side_effect=Exception(test_error_message) )

该方法的实现也很简单;为 `side_effect` 指定要引发的异常类实例即可你只需要
如果被测方法在发生异常时会执行某种处理,你可以使用此方法将测试引导至异常处理路径,从而提高测试覆盖率。

概括

感觉如何?
在这篇博客中,我们介绍了一些方法和实现,并按使用场景进行了分类。我认为它们都很简单易行。
另外,正如我在开头提到的,本文内容仅限于我个人认为值得记住的部分,因此,还有很多特性和方法我没有在这里介绍。如果
您感兴趣,请参考官方文档,以找到更好的实现和更有用的功能!
今天就到这里!下次见!

如果您觉得这篇文章对您有帮助,请点个“赞”!
7
加载中...
7票,平均分:1.00/17
3,037
X Facebook Hatena书签 口袋

这篇文章的作者

关于作者

福井宏人

我于2020年6月加入Beyond公司,在系统开发部(横滨办公室)工作。
我的工作主要涉及PHP,负责游戏API、Web系统和Shopify私有应用的开发。
我喜欢音乐,尤其偏爱西方音乐,业余爱好是弹吉他。我最喜欢的电视节目是《侦探!夜间侦探》和《外貌!Admatic Heaven》。