Python单元测试中对象打补丁的三种方式与实战详解

Finn
Finn
发布时间:2026-10-14 | 4人浏览过
原创

本文详解Python单元测试中如何使用[[wp:a0]]函数对指定对象进行打补丁,以验证调用参数或属性访问。文章介绍了[[wp:a1]]作为装饰器、上下文管理器及手动使用的三种方式,并通过示例演示如何替换原有对象为[[wp:a11]]实例,从而在测试中模拟Web请求等外部依赖,确保测试的独立性与可预测性。

问题背景与解决方案概述

问题

在编写单元测试时,通常需要为特定的对象应用补丁,以便断言其在测试过程中的预期行为,例如验证调用时的参数数量或检查特定属性的访问情况。

解决方案

可以使用unittest.mock.patch() 函数来解决这一问题。patch() 还可以作为装饰器、上下文管理器使用,或者单独调用(尽管这种情况较少见)。以下是一个将其用作装饰器的示例:


from unittest.mock import patch
import example

@patch('example.func')
def test1(x, mock_func):
  example.func(x)    # Uses patched example.func
  mock_func.assert_called_with(x)

它同样可以作为一种上下文管理器使用:


with patch('example.func') as mock_func:
  example.func(x)   # Uses patched example.func
  mock_func.assert_called_with(x)

最后,你也可以通过手动方式执行补丁操作:


p = patch('example.func')
mock_func = p.start()
example.func(x)
mock_func.assert_called_with(x)
p.stop()

如果条件允许,你可以组合使用装饰器和上下文管理器来对多个对象进行打补丁。例如:


@patch('example.func1')
@patch('example.func2')
@patch('example.func3')
def test1(mock1, mock2, mock3):
  ...

def test2():
  with patch('example.patch1') as mock1, \
     patch('example.patch2') as mock2, \
     patch('example.patch3') as mock3:
  ...

补丁机制原理与模拟对象使用

讨论

patch() 接受一个已存在对象的全路径名称,并将其替换为一个新的值。在装饰器函数或上下文管理器执行完毕后,原始值会自动恢复。默认情况下,所有被替换的值都会被MagicMock实例所替代。例如:


>>> x = 42
>>> with patch('__main__.x'):
...   print(x)
...
<MagicMock name='x' id='4314230032'>
>>> x
42
>>>

不过,你可以向patch() 提供第二个参数,从而将值替换为你所需的任何内容:


>>> x
42
>>> with patch('__main__.x', 'patched_value'):
...   print(x)
...
patched_value
>>> x
42
>>>

用作替换值的MagicMock实例能够模拟可调用对象及其实例。它们会记录对象的使用信息,并允许你执行断言检查,例如:


>>> from unittest.mock import MagicMock
>>> m = MagicMock(return_value = 10)
>>> m(1, 2, debug=True)
10
>>> m.assert_called_with(1, 2, debug=True)
>>> m.assert_called_with(1, 2)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File ".../unittest/mock.py", line 726, in assert_called_with
  raise AssertionError(msg)
AssertionError: Expected call: mock(1, 2)
Actual call: mock(1, 2, debug=True)
>>>

>>> m.upper.return_value = 'HELLO'
>>> m.upper('hello')
'HELLO'
>>> assert m.upper.called

>>> m.split.return_value = ['hello', 'world']
>>> m.split('hello world')
['hello', 'world']
>>> m.split.assert_called_with('hello world')
>>>

>>> m['blah']
<MagicMock name='mock.__getitem__()' id='4314412048'>
>>> m.__getitem__.called
True
>>> m.__getitem__.assert_called_with('blah')
>>>

实战案例:模拟网络请求

通常,这些操作会在单元测试环境中完成。假设你拥有如下函数:


# example.py
from urllib.request import urlopen
import csv

def dowprices():
  u = urlopen('http://finance.yahoo.com/d/quotes.csv?s=@^DJI&f=sl1')
  lines = (line.decode('utf-8') for line in u)
  rows = (row for row in csv.reader(lines) if len(row) == 2)
  prices = { name:float(price) for name, price in rows }
  return prices

正常情况下,该函数会调用urlopen() 从网络获取数据并进行解析。在单元测试中,你可以为其提供一个预定义的数据集。以下是使用补丁操作的示例:


import unittest
from unittest.mock import patch
import io
import example

sample_data = io.BytesIO(b'''\
"IBM",91.1\r
"AA",13.25\r
"MSFT",27.72\r
\r
''')

class Tests(unittest.TestCase):
  @patch('example.urlopen', return_value=sample_data)
  def test_dowprices(self, mock_urlopen):
    p = example.dowprices()
    self.assertTrue(mock_urlopen.called)
    self.assertEqual(p,
             {'IBM': 91.1,
             'AA': 13.25,
             'MSFT' : 27.72})

if __name__ == '__main__':
  unittest.main()

在此示例中,example模块中的urlopen() 函数被一个模拟对象替代,该对象返回包含测试数据的ByteIO()。

此外,在打补丁时我们使用了example.urlopen 来代替urllib.request.urlopen 。在创建补丁时,必须使用测试代码中的名称。由于测试代码中使用了from urllib.request import urlopen ,因此dowprices() 函数内部使用的urlopen() 函数实际上位于example模块中。

本节仅对unittest.mock 模块进行了简要介绍。如需了解更多高级特性,请参阅官方文档。

以上就是Python如何在单元测试中给对象打补丁的详细内容,更多关于Python 单元测试的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表