[Python]仅此而已! pytest模拟的摘要!

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

2025 年(我写这篇博客的年份)的星座是“蛇”,因此
想介绍一下Python 测试框架pytest

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

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

介绍

[介绍]

pytest-mock的 pytest 插件
为例,介绍如何与 Python 标准库中的unittest.mock不同
本文unittest.mock ,如果您想了解更多信息,请参考以下 Python 官方文档:
https://docs.python.org/3/library/unittest.mock.html

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

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

以下是一个在测试方法中实现的模拟示例,但模拟参数是
由 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_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`中获取
获取的值是一个元组,可以作为位置参数获取。
下面展示了一个示例实现。

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` 指定要引发的异常类实例即可。
如果被测方法在发生异常时会进行某种处理,则可以使用此方法在发生异常时跳过处理流程,从而提高测试覆盖率。

概括

你觉得怎么样?
在这篇博客中,我介绍了一些方法以及根据使用场景可以采用的实现方式,但我相信代码本身很简单,也很容易实现。
另外,正如我在开头提到的,这篇文章仅限于作者认为需要记住的重要内容,因此,还有很多函数和方法没有在这里介绍。
如果你感兴趣,请参考官方文档,寻找更好的实现方式和更有用的函数!
今天就到这里!下次见!

如果您觉得这篇文章有帮助,请点赞!
6
加载中...
6 票,平均:1.00 / 16
2,258
X Facebook 哈特纳书签 口袋

写这篇文章的人

关于作者

福井宏人

2020年6月加入Beyond。
在系统开发部(横滨办事处)工作。 在工作中,我主要负责游戏API和Web系统的开发,以及Shopify私有应用程序的开发,主要使用PHP。
我总体上喜欢音乐,主要是西方音乐,并以弹吉他为爱好。 他最喜欢的电视节目是《侦探!夜景》和《闹鬼!广告街天堂》。