我在Python中有以下代码:
library.py :
class HasSideEffects(object):
def __init__(self, kittens):
print 'kittens cry' # <- side effect
def launch_nukes(self):
print 'launching nukes' # <- side effect
my_code.py :
from library import HasSideEffects
class UtilitySubclass(HasSideEffects):
def party(self):
self.launch_nukes()
return 'confetti' # I want to test that my confetti cannon will work
if __name__ == '__main__':
x = UtilitySubclass(1) # oh no! crying kittens :(
x.party() # oh no! nukes!
我想进行单元测试,当我致电party
时,我会得到confetti
。但是,我想避免作为我正在子类化的库类的一部分的所有副作用。更具体地说,我想在单元测试中避免这些副作用,但是在生产代码中需要副作用。
我无法更改library.py
中的代码,因为它是一个库,我没有编写代码。显然,我不想仅仅为了便于单元测试而维护一个分叉版本的库。
我一直在试图模仿HasSideEffects
超类。我需要在超类中模拟__init__
和launch_nukes
,以便它们不再执行副作用。我在模拟__init__
方法时遇到了一些麻烦,显然Python mock
库不支持它?
测试party
返回confetti
的最佳方法是什么,同时避免副作用?
答案 0 :(得分:3)
也许你非常接近解决方案:
我一直在尝试模拟HasSideEffects超类。我需要在超类中模拟 init 和launch_nukes,以便它们不再执行副作用。我在模拟 init 方法时遇到了一些麻烦,显然Python模拟库不支持它?
unittest.mock
and legacy mock
的设计完全是为了做这种工作。当您需要从库或资源中删除依赖项时,模拟它并由patch
替换是非常强大的,尤其是当您应该使用无法更改的遗留代码时。
在您的情况下,您需要修补__init__
方法:要做到这一点,您必须考虑两件事
__init__
而不是类定义。__init__
方法必须返回None
和你的模拟。现在回到你的简单示例:我稍微改变了一下,使测试更加明确。
<强> library.py 强>
class HasSideEffects(object):
def __init__(self, kittens):
raise Exception('kittens cry') # <- side effect
def launch_nukes(self):
raise Exception('launching nukes') # <- side effect
<强> my_code.py 强>
from library import HasSideEffects
class UtilitySubclass(HasSideEffects):
def party(self):
self.launch_nukes()
return 'confetti' # I want to test that my confetti cannon will work
<强> test_my_code.py 强>
import unittest
from unittest.mock import patch, ANY
from my_code import UtilitySubclass
class MyTestCase(unittest.TestCase):
def test_step_by_step(self):
self.assertRaises(Exception, UtilitySubclass) #Normal implementation raise Exception
#Pay attention to return_value MUST be None for all __init__ methods
with patch("library.HasSideEffects.__init__", autospec=True, return_value=None) as mock_init:
self.assertRaises(TypeError, UtilitySubclass) #Wrong argument: autospec=True let as to catch it
us = UtilitySubclass("my kittens") #Ok now it works
#Sanity check: __init__ call?
mock_init.assert_called_with(ANY, "my kittens") #Use autospec=True inject self as first argument -> use Any to discard it
#But launch_nukes() was still the original one and it will raise
self.assertRaises(Exception, us.party)
with patch("library.HasSideEffects.launch_nukes") as mock_launch_nukes:
self.assertEqual("confetti",us.party())
# Sanity check: launch_nukes() call?
mock_launch_nukes.assert_called_with()
@patch("library.HasSideEffects.launch_nukes")
@patch("library.HasSideEffects.__init__", autospec=True, return_value=None)
def test_all_in_one_by_decorator(self, mock_init, mock_launch_nukes):
self.assertEqual("confetti",UtilitySubclass("again my kittens").party())
mock_init.assert_called_with(ANY, "again my kittens")
mock_launch_nukes.assert_called_with()
if __name__ == '__main__':
unittest.main()
请注意,装饰器版本简洁明了。
答案 1 :(得分:1)
这是一个非常棘手的问题。如果您正在模拟的库类很简单,那么您可以将您的功能作为 mixin 提供。
class UtilitySubclassMixin(object):
def party(self):
self.launch_nukes()
return 'confetti'
class UtilitySubclass(library.HasSideEffects, UtilityClassMixin):
"""meaningful docstring."""
现在,为了进行测试,您只需要提供一个具有您的UtililtySubclassMixin所期望的接口的子类(例如,具有launch_nukes
方法的接口)。这不是理想的,但是如果你需要模拟一个带有副作用的__init__
的库方法,它就是你能做的最好的方法。如果它是所有具有副作用的非魔法方法,unittest.mock
可用于直接修补library.HasSideEffects
上的方法。
而且,FWIW,这是__init__
方法永远不会产生副作用的一个很好的理由: - )。