各位靓仔靓女们,晚上好!今天咱来聊聊Python单元测试里一个很实用、但也容易让人头大的话题:Mocking。别怕,保证咱用最接地气的方式,把unittest.mock
和pytest-mock
这两个好伙伴给盘明白。
开场白:别让外部依赖拖你后腿
想象一下,你写了一个超牛的函数,功能强大,逻辑清晰。但是,它需要连接数据库,或者调用一个外部API。问题来了:
- 数据库挂了怎么办? 测试总是连不上数据库,或者数据库里没数据,测试就过不了。
- API收费了怎么办? 免费API突然要收费,或者API每天调用次数有限制,测试总失败。
- 外部服务不稳定怎么办? 网络不稳定,外部服务时好时坏,测试结果忽上忽下,让人怀疑人生。
这些外部依赖就像绊脚石,让你的单元测试寸步难行。这时候,Mocking就该闪亮登场了!
什么是Mocking?
简单来说,Mocking就是用“假货”代替“真货”。在单元测试中,我们用Mock对象来模拟外部依赖的行为,让你的函数只关注自己的核心逻辑,不受外部因素的干扰。
unittest.mock
:Python自带的Mock神器
Python标准库自带了unittest.mock
模块,提供了强大的Mocking功能。咱们先从它入手,一步一步来。
-
Mock
类:最基础的模拟对象Mock
类可以模拟任何对象,包括函数、类、模块,甚至属性。from unittest.mock import Mock # 模拟一个函数 my_func = Mock(return_value=42) result = my_func(1, 2, key='value') print(result) # 输出: 42 print(my_func.called) # 输出: True print(my_func.call_args) # 输出: ((1, 2), {'key': 'value'}) print(my_func.call_count) # 输出: 1
return_value
:指定Mock对象的返回值。called
:判断Mock对象是否被调用过。call_args
:获取Mock对象的调用参数(位置参数和关键字参数)。call_count
:获取Mock对象的调用次数。
是不是很简单? 你可以给
Mock
对象设置各种属性,模拟各种行为。 -
patch
装饰器:替换真实对象patch
装饰器可以临时替换某个对象,在测试结束后自动恢复。这简直是单元测试的福音!from unittest.mock import patch def get_data_from_api(): # 假设这个函数调用了外部API import requests response = requests.get('https://api.example.com/data') return response.json() @patch('__main__.requests.get') # 注意这里的路径 '__main__.requests.get' def test_get_data_from_api(mock_get): # 配置Mock对象的行为 mock_get.return_value.json.return_value = {'data': 'mocked data'} # 调用被测试的函数 data = get_data_from_api() # 断言 assert data == {'data': 'mocked data'} mock_get.assert_called_once_with('https://api.example.com/data')
@patch('__main__.requests.get')
:patch
装饰器接受一个字符串参数,指定要替换的对象。注意这里的路径需要写清楚,包括模块名和函数名。__main__
是当前模块的名字。mock_get
:patch
装饰器会将Mock对象作为参数传递给测试函数。mock_get.return_value.json.return_value = {'data': 'mocked data'}
:模拟API返回的JSON数据。注意,这里需要一层一层地设置返回值,直到到达最终的返回值。mock_get.assert_called_once_with('https://api.example.com/data')
:断言requests.get
函数被调用了一次,并且参数是'https://api.example.com/data'
。
patch
装饰器还有很多用法,比如可以替换类的属性、模块中的变量等等。 -
side_effect
:模拟更复杂的行为side_effect
可以让你模拟更复杂的行为,比如抛出异常、返回不同的值等等。from unittest.mock import Mock def my_func(x): if x > 0: return "Positive" elif x < 0: return "Negative" else: return "Zero" mock_func = Mock(side_effect=my_func) print(mock_func(1)) # 输出: Positive print(mock_func(-1)) # 输出: Negative print(mock_func(0)) # 输出: Zero # 模拟抛出异常 def raise_exception(*args, **kwargs): raise ValueError("Something went wrong!") mock_func = Mock(side_effect=raise_exception) try: mock_func(1) except ValueError as e: print(e) # 输出: Something went wrong!
side_effect
可以是一个函数,每次调用Mock对象时,都会执行这个函数,并返回它的返回值。side_effect
也可以是一个异常,每次调用Mock对象时,都会抛出这个异常。side_effect
还可以是一个迭代器,每次调用Mock对象时,都会返回迭代器的下一个值。
pytest-mock
:Pytest的Mock好帮手
pytest-mock
是一个pytest插件,它封装了unittest.mock
,提供了更简洁、更方便的Mocking API。
-
mocker
fixture:pytest的Mock对象pytest-mock
提供了一个名为mocker
的fixture,你可以直接在测试函数中使用它。import pytest from unittest.mock import Mock def my_func(): # 假设这个函数调用了外部函数 return external_func() def external_func(): # 这是外部函数,我们需要mock它 return "Real data" def test_my_func(mocker): # 创建Mock对象 mock_external_func = mocker.patch('__main__.external_func', return_value="Mocked data") # 调用被测试的函数 result = my_func() # 断言 assert result == "Mocked data" mock_external_func.assert_called_once()
mocker.patch('__main__.external_func', return_value="Mocked data")
:使用mocker.patch
方法替换external_func
函数,并设置返回值。mock_external_func.assert_called_once()
:断言external_func
函数被调用了一次。
pytest-mock
还提供了其他一些方便的方法,比如:mocker.spy()
:可以监视函数的调用情况,但不会替换它。mocker.stub()
:创建一个简单的占位符对象。
-
mocker.patch.object
:Mock对象的属性
针对类的属性进行Mock,通常会使用mocker.patch.object
。import pytest from unittest.mock import Mock class MyClass: def __init__(self, value): self.value = value def get_value(self): return self.value def test_get_value(mocker): instance = MyClass(10) mocker.patch.object(instance, 'value', new_value=20) assert instance.get_value() == 20
这个例子中,
mocker.patch.object
被用来修改MyClass
实例的value
属性。
Mocking 的最佳实践
- 只Mock你需要的:不要过度Mock。只Mock那些影响你的核心逻辑的外部依赖。
- 保持测试的简洁:Mock代码应该清晰易懂,不要让Mock代码比被测试的代码还复杂。
- 验证Mock对象的行为:确保你的Mock对象被正确地调用,并且返回了正确的值。
- 使用上下文管理器:对于复杂的Mock场景,可以使用
with
语句来管理Mock对象的生命周期。
总结:Mocking让单元测试更靠谱
功能 | unittest.mock |
pytest-mock |
优点 | 缺点 |
---|---|---|---|---|
核心类/方法 | Mock , patch , side_effect |
mocker.patch , mocker.spy , mocker.stub |
Python自带,无需安装;功能强大,灵活性高 | 语法相对繁琐 |
集成 | 与unittest 框架集成 |
与pytest 框架集成 |
与pytest 无缝集成,使用方便;语法更简洁 |
需要安装;相比unittest.mock ,灵活性稍差 |
使用场景 | 适用于任何Python项目,尤其是使用unittest 框架的项目 |
适用于使用pytest 框架的项目,追求简洁、方便的Mocking方式 |
无 | 无 |
Mocking是单元测试中不可或缺的一部分。它可以让你隔离外部依赖,专注于测试核心逻辑,提高测试的可靠性和效率。无论是unittest.mock
还是pytest-mock
,都是非常优秀的Mocking工具。选择哪个取决于你的项目和个人偏好。
记住,Mocking的目的是让你的测试更简单、更可靠。不要让Mocking本身成为负担。希望今天的分享能帮助你更好地掌握Mocking技术,写出更棒的Python代码!
今天就到这里,各位晚安!