如何用mock修补模块内部函数?

31 投票
5 回答
32394 浏览
提问于 2025-04-16 13:54

这里提到的“内部函数”,指的是在定义它的同一个模块里被调用的函数。

我在单元测试中使用了mock库,特别是patch装饰器。这些测试是Django的单元测试,但这对任何Python测试都适用。

我有一个模块,里面有好几个函数,它们之间会相互调用。例如(这段代码是虚构的,忽略小数点Decimal的部分):

TAX_LOCATION = 'StateName, United States'

def add_tax(price, user):
    tax = 0
    if TAX_LOCATION == 'StateName, UnitedStates':
        tax = price * .75
    return (tax, price+tax)

def build_cart(...):
    # build a cart object for `user`
    tax, price = add_tax(cart.total, cart.user)
    return cart

这些函数是一个更深的调用链的一部分(func1 -> func2 -> build_cart -> add_tax),它们都在同一个模块里。

在我的单元测试中,我想禁用税费,以便得到一致的结果。对我来说,有两个选择:1)把TAX_LOCATION替换成一个空字符串,这样add_tax就不会做任何事情;或者2)把add_tax替换成直接返回(0, price)。

但是,当我尝试替换这两个函数时,替换在外部似乎有效(我可以在测试中导入被替换的部分并打印出来,得到预期的值),但在内部似乎没有效果(代码的结果表现得好像没有进行替换一样)。

我的测试代码是这样的(同样是虚构的):

from mock import patch
from django.test import TestCase

class MyTests(TestCase):

    @patch('mymodule.TAX_LOCATION', '')
    def test_tax_location(self):
        import mymodule
        print mymodule.TAX_LOCATION # ''
        mymodule.func1()
        self.assertEqual(cart.total, original_price) # fails, tax applied

    @patch('mymodule.add_tax', lambda p, u: (0, p))
    def test_tax_location(self):
        import mymodule
        print mymodule.add_tax(50, None) # (0, 50)
        mymodule.func1()
        self.assertEqual(cart.total, original_price) # fails, tax applied

有没有人知道mock是否可以替换像这样在内部使用的函数,还是说我没戏了?

5 个回答

4

我想提供一个除了被接受的答案之外的解决方案。你还可以在其他模块导入之前,先对这个模块进行修补,然后在测试结束时再把修补去掉。

#import some modules that don't use module you are going to patch
import unittest
from mock import patch
import json
import logging
...


patcher = patch('some.module.path.function', lambda x: x)
patcher.start()

import some.module.path

class ViewGetTests(unittest.TestCase):

  @classmethod
  def tearDownClass(cls):
      patcher.stop()
16

另一个选择是明确地对这个函数调用补丁(patch):

mock.patch('function_name')

并且要支持直接运行或者通过 py.test 等工具运行:

mock.patch(__name__ + '.' + 'function_name')
23

答案是:整理你的导入代码

@patch('mymodule.TAX_LOCATION', '') 确实正确地进行了修改,但当时我们的导入方式非常混乱——有时候我们导入的是 mymodule.build_cart,有时候导入的是 project.mymodule.build_cart——所以“完整”的导入根本没有被修改。Mock 不能自动知道这两种不同的导入方式……除非我们明确告诉它。

现在我们已经统一了所有的导入方式,使用了更长的路径,这样一来,程序的表现就好多了。

撰写回答