覆盖python模块中的“私有”方法

时间:2015-04-02 09:05:08

标签: python testing mocking override private

我想在python中测试一个函数,但它依赖于模块级"私有"功能,我不想要打电话,但我无法覆盖/嘲笑它。情形:

module.py

_cmd(command, args):
  # do something nasty

function_to_be_tested():
  # do cool things
  _cmd('rm', '-rf /')
  return 1

test_module.py

import module
test_function():
  assert module.function_to_be_tested() == 1

理想情况下,在此测试中我不想调用_cmd。我已经看了一些其他主题,我已经尝试了以下但没有运气:

 test_function():
   def _cmd(command, args):
     # do nothing
     pass
   module._cmd = _cmd

虽然针对_cmd检查module._cmd并不能给出正确的引用。使用mock:

from mock import patch

def _cmd_mock(command, args):
  # do nothing
  pass

@patch('module._cmd', _cmd_mock) 
test_function():
   ...
检查module._cmd时,

给出了正确的引用,尽管`function_to_be_tested'仍然使用原始的_cmd(由它做出令人讨厌的事情证明)。

这很棘手,因为_cmd是一个模块级函数,我不想把它移到模块中

1 个答案:

答案 0 :(得分:2)

<强> [免责声明]

此问题中发布的合成示例有效,所描述的问题来自生产代码中的特定实现。也许这个问题应该作为主题关闭,因为问题不可重复。


[注意] 对于不耐烦的人解决方案就在答案的最后。


无论如何,这个问题给了我一个很好的思考点:当我们无法访问引用所在的变量时,我们如何修补方法引用?

很多次我发现了这样的问题。有很多方法可以满足这种情况,公共场所是

  • 装饰器:我们要替换的实例作为装饰器参数传递或在装饰器静态实现中使用
  • 我们想要修补的是方法的默认参数

在这两种情况下,重构代码可能是最好的方式,但如果我们正在使用一些遗留代码或者装饰器是第三部分装饰器呢?

好吧,我们背靠墙但是我们正在使用python而在python中没有什么是不可能的。我们需要的只是修补函数/方法的参考,而不是修补它的引用,我们可以修补__code__:是的我正在谈论修补字节码而不是函数。

得到一个真实的例子。我使用的默认参数大小写很简单,但它可以在装饰器的情况下工作。

def cmd(a):
    print("ORIG {}".format(a))
def cmd_fake(a):
    print("NEW {}".format(a))
def do_work(a, c=cmd):
    c(a)

do_work("a")
cmd=cmd_fake
do_work("b")

输出:

ORIG a
ORIG b

Ok在这种情况下,我们可以通过传递do_work来测试cmd_fake但是在某些情况下不可能做到这一点:例如,如果我们需要调用类似的东西呢?

def what_the_hell():
    list(map(lambda a:do_work(a), ["c","d"]))

我们可以做的是通过

修补cmd.__code__而不是_cmd
cmd.__code__ = cmd_fake.__code__

所以请按照代码

do_work("a")
what_the_hell()
cmd.__code__ = cmd_fake.__code__
do_work("b")
what_the_hell()

提供以下输出:

ORIG a
ORIG c
ORIG d
NEW b
NEW c
NEW d

此外,如果我们想要使用模拟,我们可以通过添加以下行来实现:

from unittest.mock import Mock, call
cmd_mock = Mock()
def cmd_mocker(a):
    cmd_mock(a)

cmd.__code__=cmd_mocker.__code__
what_the_hell()
cmd_mock.assert_has_calls([call("c"),call("d")])
print("WORKS")

打印出来

WORKS

也许我已经完成了...但OP还在等待他的问题的解决方案

from mock import patch, Mock

cmd_mock = Mock()
#A closure for grabbing the right function code
def cmd_mocker(a):
    cmd_mock(a)

@patch.object(module._cmd,'__code__', new=cmd_mocker.__code__) 
test_function():
   ...

现在我应该说从不使用这个技巧,除非你背对墙。测试应该很容易理解和调试...尝试调试这样的东西,你会变得疯狂!