好的,各位听众,欢迎来到今天的“Mock一把梭,Bug全溜走”技术讲座!我是你们的老朋友,bug终结者,今天咱们来聊聊unittest.mock.patch
这个神奇的家伙。
开场白:为什么我们需要Mock?
想象一下,你写了一个很棒的函数,它负责从数据库里读取数据,然后进行一些复杂的计算。但是,现在数据库出了点小问题,或者你根本不想每次测试都真的去访问数据库,这时候怎么办呢?难道要对着数据库祈祷吗?当然不是!
这时候,Mock就闪亮登场了。简单来说,Mock就是用一个假的、可控的对象来代替真实的对象。这样,你就可以在测试中完全控制这些依赖项的行为,从而更专注于测试你的代码逻辑本身。
unittest.mock.patch
:你的Mock瑞士军刀
在Python的unittest.mock
模块中,patch
绝对是核心角色。它可以像一把瑞士军刀一样,让你轻松地模拟类、方法、属性,甚至整个模块。
1. patch
的基本用法:函数装饰器
最常见的用法是作为函数装饰器。假设我们有一个函数get_data_from_api
,它依赖于一个requests
库来获取数据:
import requests
def get_data_from_api(url):
response = requests.get(url)
response.raise_for_status() # 抛出异常如果状态码不是200
return response.json()
现在,我们不想真的去访问API,而是想模拟requests.get
的行为。我们可以这样做:
import unittest
from unittest.mock import patch
class TestGetDataFromApi(unittest.TestCase):
@patch('requests.get') # 告诉patch要模拟的是requests.get
def test_get_data_from_api_success(self, mock_get): # mock_get 是模拟的 requests.get 对象
# 配置mock对象的行为
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {'data': 'some data'}
# 调用被测试的函数
result = get_data_from_api('http://example.com/api')
# 断言
self.assertEqual(result, {'data': 'some data'})
mock_get.assert_called_once_with('http://example.com/api')
这段代码做了什么?
@patch('requests.get')
:这是一个装饰器,它告诉patch
要模拟requests
模块的get
函数。def test_get_data_from_api_success(self, mock_get)
:注意,测试函数接收了一个参数mock_get
,这个参数就是被模拟的requests.get
对象。mock_get.return_value.status_code = 200
和mock_get.return_value.json.return_value = {'data': 'some data'}
:这两行代码配置了模拟对象的行为。我们设置了status_code
为200,并且让json()
方法返回了我们想要的数据。result = get_data_from_api('http://example.com/api')
:调用被测试的函数。self.assertEqual(result, {'data': 'some data'})
:断言结果是否符合预期。mock_get.assert_called_once_with('http://example.com/api')
:断言mock_get
是否被调用过,并且调用时的参数是否正确。
2. patch
作为上下文管理器
除了作为装饰器,patch
还可以作为上下文管理器使用。这在需要更精细地控制模拟对象的生命周期时非常有用。
import unittest
from unittest.mock import patch
class TestGetDataFromApiContextManager(unittest.TestCase):
def test_get_data_from_api_failure(self):
with patch('requests.get') as mock_get:
mock_get.side_effect = requests.exceptions.RequestException('Simulated error')
with self.assertRaises(requests.exceptions.RequestException):
get_data_from_api('http://example.com/api')
这段代码做了什么?
with patch('requests.get') as mock_get:
:使用patch
作为上下文管理器,mock_get
在with
语句块中有效。mock_get.side_effect = requests.exceptions.RequestException('Simulated error')
:设置side_effect
属性,让mock_get
在被调用时抛出一个异常。with self.assertRaises(requests.exceptions.RequestException):
:断言代码是否抛出了预期的异常。
3. patch
的参数详解:new
,create
,spec
,autospec
patch
有很多可选参数,可以让你更灵活地控制模拟行为。
-
new
: 用于指定替换的对象。你可以传递一个值,一个对象,或者一个类。import unittest from unittest.mock import patch class MyClass: def my_method(self): return "Original" def new_function(): return "Mocked" class TestPatchNew(unittest.TestCase): def test_patch_with_new_function(self): with patch('__main__.MyClass.my_method', new=new_function) as mock_method: instance = MyClass() result = instance.my_method() self.assertEqual(result, "Mocked") def test_patch_with_new_value(self): with patch('__main__.MyClass.my_method', new="Mocked Value") as mock_method: instance = MyClass() result = instance.my_method() self.assertEqual(result, "Mocked Value")
在这个例子中,我们使用
new
参数来将MyClass.my_method
替换为一个新的函数new_function
和一个字符串 "Mocked Value"。 -
create
: 如果你尝试模拟一个不存在的属性,patch
默认会抛出一个AttributeError
。但是,如果设置create=True
,patch
会自动创建这个属性。import unittest from unittest.mock import patch class MyClass: pass class TestPatchCreate(unittest.TestCase): def test_patch_create(self): with patch('__main__.MyClass.non_existent_attribute', create=True) as mock_attribute: mock_attribute.return_value = "Created Attribute" instance = MyClass() result = instance.non_existent_attribute() self.assertEqual(result, "Created Attribute")
在这个例子中,
MyClass
没有non_existent_attribute
属性。通过设置create=True
,patch
会自动创建这个属性。 -
spec
:spec
参数可以让你指定一个对象作为模拟对象的“模板”。模拟对象会拥有和模板对象相同的属性和方法。这可以帮助你避免拼写错误,并且让你的测试更贴近真实环境。import unittest from unittest.mock import patch class MyClass: def my_method(self, arg1, arg2): return arg1 + arg2 class TestPatchSpec(unittest.TestCase): def test_patch_spec(self): with patch('__main__.MyClass.my_method', spec=MyClass.my_method) as mock_method: mock_method.return_value = "Mocked Result" instance = MyClass() result = instance.my_method("a", "b") self.assertEqual(result, "Mocked Result") mock_method.assert_called_with("a", "b")
在这个例子中,我们使用
spec=MyClass.my_method
来指定mock_method
应该具有与MyClass.my_method
相同的接口。 -
autospec
:autospec
是spec
的更智能版本。它会自动根据被模拟对象的签名来创建模拟对象。这意味着,如果被模拟对象的签名发生变化,你的测试会立即失败,从而帮助你及时发现问题。import unittest from unittest.mock import patch class MyClass: def my_method(self, arg1, arg2): return arg1 + arg2 class TestPatchAutospec(unittest.TestCase): def test_patch_autospec(self): with patch('__main__.MyClass.my_method', autospec=True) as mock_method: mock_method.return_value = "Mocked Result" instance = MyClass() result = instance.my_method("a", "b") self.assertEqual(result, "Mocked Result") mock_method.assert_called_with("a", "b") def test_patch_autospec_wrong_arguments(self): with patch('__main__.MyClass.my_method', autospec=True) as mock_method: instance = MyClass() with self.assertRaises(TypeError): instance.my_method("a") # 缺少一个参数,会抛出TypeError
在这个例子中,我们使用
autospec=True
来让mock_method
自动具有与MyClass.my_method
相同的签名。如果我们在调用instance.my_method
时传递了错误的参数数量,测试将会失败。
4. side_effect
:让你的Mock更灵活
side_effect
是一个非常强大的属性,它可以让你定义模拟对象在被调用时产生的副作用。
-
抛出异常:
import unittest from unittest.mock import patch class TestSideEffectException(unittest.TestCase): def test_side_effect_exception(self): with patch('requests.get') as mock_get: mock_get.side_effect = Exception("Simulated Error") with self.assertRaises(Exception): requests.get("http://example.com")
在这个例子中,我们设置
mock_get
的side_effect
为一个异常。当requests.get
被调用时,会抛出这个异常。 -
返回不同的值:
import unittest from unittest.mock import patch class TestSideEffectIterable(unittest.TestCase): def test_side_effect_iterable(self): with patch('requests.get') as mock_get: mock_get.side_effect = [1, 2, 3] self.assertEqual(requests.get("http://example.com"), 1) self.assertEqual(requests.get("http://example.com"), 2) self.assertEqual(requests.get("http://example.com"), 3)
在这个例子中,我们设置
mock_get
的side_effect
为一个列表。每次requests.get
被调用时,会依次返回列表中的值。 -
执行自定义函数:
import unittest from unittest.mock import patch def my_side_effect(arg): return arg * 2 class TestSideEffectFunction(unittest.TestCase): def test_side_effect_function(self): with patch('requests.get') as mock_get: mock_get.side_effect = my_side_effect self.assertEqual(requests.get(5), 10)
在这个例子中,我们设置
mock_get
的side_effect
为一个自定义函数my_side_effect
。每次requests.get
被调用时,会执行这个函数,并将函数的返回值作为requests.get
的返回值。
5. assert_called
系列方法:验证你的Mock是否被正确调用
unittest.mock
提供了一系列assert_called
方法,可以让你验证模拟对象是否被调用过,以及调用时的参数是否正确。
assert_called()
: 验证模拟对象是否被调用过。assert_called_once()
: 验证模拟对象是否只被调用过一次。- *`assert_called_with(args, kwargs)`: 验证模拟对象是否被调用过,并且调用时的参数是否正确。
- *`assert_called_once_with(args, kwargs)`: 验证模拟对象是否只被调用过一次,并且调用时的参数是否正确。
- *`assert_any_call(args, kwargs)`: 验证模拟对象是否被调用过,并且至少有一次调用时的参数符合要求。
assert_has_calls(calls, any_order=False)
: 验证模拟对象是否按照指定的顺序被调用过。
import unittest
from unittest.mock import patch, call
class MyClass:
def my_method(self, arg1, arg2):
pass
class TestAssertCalled(unittest.TestCase):
def test_assert_called(self):
with patch('__main__.MyClass.my_method') as mock_method:
instance = MyClass()
instance.my_method("a", "b")
self.assertTrue(mock_method.called)
mock_method.assert_called()
def test_assert_called_once(self):
with patch('__main__.MyClass.my_method') as mock_method:
instance = MyClass()
instance.my_method("a", "b")
mock_method.assert_called_once()
def test_assert_called_with(self):
with patch('__main__.MyClass.my_method') as mock_method:
instance = MyClass()
instance.my_method("a", "b")
mock_method.assert_called_with("a", "b")
def test_assert_called_once_with(self):
with patch('__main__.MyClass.my_method') as mock_method:
instance = MyClass()
instance.my_method("a", "b")
mock_method.assert_called_once_with("a", "b")
def test_assert_any_call(self):
with patch('__main__.MyClass.my_method') as mock_method:
instance = MyClass()
instance.my_method("a", "b")
instance.my_method("c", "d")
mock_method.assert_any_call("a", "b")
def test_assert_has_calls(self):
with patch('__main__.MyClass.my_method') as mock_method:
instance = MyClass()
instance.my_method("a", "b")
instance.my_method("c", "d")
calls = [call("a", "b"), call("c", "d")]
mock_method.assert_has_calls(calls)
def test_assert_has_calls_any_order(self):
with patch('__main__.MyClass.my_method') as mock_method:
instance = MyClass()
instance.my_method("a", "b")
instance.my_method("c", "d")
calls = [call("c", "d"), call("a", "b")]
mock_method.assert_has_calls(calls, any_order=True)
6. patch.object
:专门用于模拟对象属性
如果你只想模拟一个对象的某个属性,可以使用patch.object
。
import unittest
from unittest.mock import patch
class MyClass:
def __init__(self):
self.my_attribute = "Original Value"
def get_attribute(self):
return self.my_attribute
class TestPatchObject(unittest.TestCase):
def test_patch_object(self):
instance = MyClass()
with patch.object(instance, 'my_attribute', "Mocked Value") as mock_attribute:
self.assertEqual(instance.get_attribute(), "Mocked Value")
在这个例子中,我们使用 patch.object
来模拟 MyClass
实例的 my_attribute
属性。
7. patch.dict
:模拟字典
patch.dict
可以让你临时替换一个字典的内容。
import unittest
from unittest.mock import patch
my_dict = {'a': 1, 'b': 2}
class TestPatchDict(unittest.TestCase):
def test_patch_dict(self):
with patch.dict(my_dict, {'a': 3, 'c': 4}):
self.assertEqual(my_dict['a'], 3)
self.assertEqual(my_dict['b'], 2)
self.assertEqual(my_dict['c'], 4)
self.assertEqual(my_dict['a'], 1)
self.assertEqual(my_dict['b'], 2)
self.assertNotIn('c', my_dict)
在这个例子中,我们使用 patch.dict
来临时替换 my_dict
的内容。在 with
语句块中,my_dict
的 a
键的值被替换为 3,并且添加了新的键 c
。当 with
语句块结束时,my_dict
恢复到原始状态。
8. Mocking Properties
在Python中,我们经常使用properties来封装对属性的访问。patch
也能很好地处理这种情况。
import unittest
from unittest.mock import patch
class MyClass:
def __init__(self, value):
self._my_property = value
@property
def my_property(self):
return self._my_property
@my_property.setter
def my_property(self, value):
self._my_property = value
class TestPatchProperty(unittest.TestCase):
def test_patch_property(self):
instance = MyClass("Original Value")
with patch('__main__.MyClass.my_property', new_callable=unittest.mock.PropertyMock) as mock_property:
mock_property.return_value = "Mocked Value"
self.assertEqual(instance.my_property, "Mocked Value")
instance.my_property = "New Value"
mock_property.assert_called_once_with("New Value")
关键点在于使用new_callable=unittest.mock.PropertyMock
。这告诉patch
我们要模拟的是一个property,而不是一个普通的方法或属性。
最佳实践和注意事项
- 尽量使用
autospec=True
: 它可以帮助你避免因接口变更而导致的测试失败。 - 只模拟你需要的: 不要过度模拟,只模拟那些你无法控制或者不想在测试中使用的依赖项。
- 保持测试的简洁性: 模拟应该让你的测试更简单,而不是更复杂。
- 注意作用域: 确保你的模拟对象在正确的作用域内生效。
- 清理模拟: 使用上下文管理器或者
stop()
方法来确保你的模拟对象在测试结束后被清理干净。
总结
unittest.mock.patch
是一个非常强大的工具,可以让你轻松地模拟各种依赖项,从而编写更可靠、更易于维护的单元测试。熟练掌握patch
的各种用法,可以让你在测试的世界里如鱼得水,bug无处遁形!
表格总结patch
常用参数
参数 | 描述 | 示例 |
---|---|---|
new |
用于指定替换的对象。可以传递一个值,一个对象,或者一个类。 | patch('module.object', new=mock_object) |
create |
如果要模拟的属性不存在,是否自动创建。默认为False 。 |
patch('module.non_existent_attribute', create=True) |
spec |
指定一个对象作为模拟对象的“模板”。模拟对象会拥有和模板对象相同的属性和方法。 | patch('module.object', spec=RealClass) |
autospec |
自动根据被模拟对象的签名来创建模拟对象。 | patch('module.object', autospec=True) |
side_effect |
设置模拟对象被调用时产生的副作用。可以是异常、返回值列表、或者自定义函数。 | patch('module.object', side_effect=Exception('Simulated error')) |
常见问题解答
-
Q: 为什么我的模拟没有生效?
- A: 检查你的
patch
目标是否正确。确保你模拟的是代码实际使用的对象。 - A: 检查你的作用域是否正确。确保模拟对象在代码执行时生效。
- A: 检查你的
-
Q: 如何模拟一个全局变量?
- A: 使用
patch('__main__.global_variable')
来模拟当前模块的全局变量。
- A: 使用
-
Q: 如何模拟一个类的静态方法或类方法?
- A: 使用
patch('module.Class.static_method')
或patch('module.Class.classmethod')
。
- A: 使用
-
Q: 什么时候应该使用
patch
,什么时候应该使用其他Mock方法?- A:
patch
适用于需要替换对象的情况。如果只需要创建Mock对象并配置其行为,可以使用Mock
类本身。
- A:
结束语
希望今天的讲座能帮助大家更好地理解和使用unittest.mock.patch
。记住,Mock不是万能的,但它绝对是你测试工具箱里不可或缺的一员。掌握了它,你就能写出更健壮、更可靠的代码,让bug无处遁形!
感谢大家的收听,下次再见!