使用 Python Mock 模拟函数

109

我想使用Python的mock模块来mock一个返回外部内容的函数。

但是,我在mock被导入到一个模块中的函数时遇到了一些问题。

例如,在util.py中:

def get_content():
  return "stuff"

我想模拟util.get_content,使其返回其他内容。

我正在尝试这样做:

util.get_content=Mock(return_value="mocked stuff")

如果 get_content 在另一个模块中被调用,它似乎从来没有返回过模拟对象。我在使用 Mock 方面有什么遗漏吗?

请注意,如果我调用以下内容,则工作正常:

>>> util.get_content=Mock(return_value="mocked stuff")
>>> util.get_content()
"mocked stuff"

然而,如果get_content被从另一个模块中调用,它会调用原始函数而不是模拟版本:

>>> from mymodule import MyObj
>>> util.get_content=Mock(return_value="mocked stuff")
>>> m=MyObj()
>>> m.func()
"stuff"

mymodule.py 的内容

from util import get_content

class MyObj:    
    def func():
        get_content()

我想问的是,如何在调用模块内部的函数时使用模拟版本?

看起来问题可能出在from module import function上,它未指向模拟函数。


我按照您的确切描述(在Linux上使用Python 2.5,Mock 0.7.0),它运行良好。您是否有更多细节可以提供? - Nate
嗯 - 当从顶层作用域调用该函数时,它似乎表现如预期。然而,当它从另一个模块或函数内部调用(即在调用堆栈中更深处)时,它不会表现出Mock的行为。我正在澄清这个例子以说明这一点。 - shreddd
好的,我尝试了你的新描述 - 即使从 mymodule.func(),我仍然得到了正确的答案。对我唯一的区别是我的 mymodule.func() 返回 util.get_content(),而不仅仅是调用它。我觉得你的描述中仍然缺少一些信息。你是否真正尝试过你上面的确切描述?你的实际代码是什么? - Nate
抱歉,我避免粘贴大段代码。你是正确的——我的简化示例并没有彻底失败,但我认为我已经部分地找到了解决方案。稍后会有更新。 - shreddd
奇怪 - 在我更复杂的Django测试用例中,事情似乎不正常,但是当我试图简化它时,Mock对象似乎按预期传递。我猜测在导入方面存在一些差异,这会创建略微不同的命名空间。 - shreddd
5个回答

63

通常情况下,可以使用来自mock的patch。请考虑以下内容:

utils.py

def get_content():
    return 'stuff'

mymodule.py

from util import get_content


class MyClass(object):

    def func(self):
        return get_content()

test.py

import unittest

from mock import patch

from mymodule import MyClass

class Test(unittest.TestCase):

    @patch('mymodule.get_content')
    def test_func(self, get_content_mock):
        get_content_mock.return_value = 'mocked stuff'

        my_class = MyClass()
        self.assertEqual(my_class.func(), 'mocked stuff')
        self.assertEqual(get_content_mock.call_count, 1)
        get_content_mock.assert_called_once()

注意到get_content被模拟,它不是util.get_content,而是mymodule.get_content,因为我们在mymodule中使用它。

以上已经经过了mock v2.0.0、nosetests v1.3.7和python v2.7.9的测试。


你对它是模块而不是工具的评论帮了我很多! - chickenman

47

我想我有一种解决方法,尽管如何解决通用情况还不是很清楚。

mymodule中,如果我替换

from util import get_content

class MyObj:    
    def func():
        get_content()

使用

import util

class MyObj:    
    def func():
        util.get_content()

Mock 似乎被调用了。看起来命名空间需要匹配(这很合理)。然而,奇怪的是我预期

import mymodule
mymodule.get_content = mock.Mock(return_value="mocked stuff")

为了解决我使用from/import语法的原始情况,其中现在将get_content导入到mymodule中。但这仍然涉及未模拟的get_content

事实证明名称空间很重要 - 写代码时需要记住这一点。


2
我真的很想知道通用情况下的答案。有时候使用“from util import get_context”语法进行模拟测试有效,有时候则无效。有人能解释一下吗? - johnboiles
1
非常感谢您的回答。我非常困惑,为什么我的模拟测试在某些文件中有效而在其他文件中无效。 - guettli
5
“@johnboiles 对于‘为什么有时候有效,有时候无效’的问题,答案在于导入模块的顺序。如果在导入之前执行模拟(mock),它会起作用;如果先执行导入,那么它就不起作用了。” - guettli

29

如果我们想要模拟一个内置函数,比如 open,该怎么办? - Cartucho

12

假设您正在模块 foobar 内创建模拟对象:

import util, mock
util.get_content = mock.Mock(return_value="mocked stuff")
如果你在没有先导入foobar的情况下,导入mymodule并调用util.get_content,那么你的模拟将不会被安装:
import util
def func()
    print util.get_content()
func()
"stuff"

改为:

import util
import foobar   # substitutes the mock
def func():
    print util.get_content()
func()
"mocked stuff"

请注意,foobar可以从任何地方导入(即A模块导入B模块再导入foobar),只要在调用util.get_content之前对foobar进行了评估。


2
虽然它并没有直接回答你的问题,但另一个可能的替代方案是使用@staticmethod将你的函数转换为静态方法。
因此,你可以使用以下类似的方式将你的模块utils转换为一个类:
class util(object):
     @staticmethod
     def get_content():
         return "stuff"

然后正确地模拟补丁。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接