如何模拟作为库一部分的超类? [英] How do a mock a superclass that is part of a library?
问题描述
我在 Python 中有一些类似如下的代码:
I have some code like the following in 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
.但是,我想避免作为我子类化的库类的一部分的所有副作用.更具体地说,我想在单元测试中避免这些副作用,但在生产代码中需要这些副作用.
I want to unit test that when I call party
I will get confetti
. However, I want to avoid all the side effects that are part of the library class I am subclassing. More specifically, I want to avoid these side effects in the unit tests, but the side effects are needed in the production code.
我无法更改 library.py
中的代码,因为它是一个库,而且我没有编写代码.显然,我不想为了方便单元测试而维护一个库的分叉版本.
I cannot change the code in library.py
because it's a library, and I did not write the code. Obviously I wouldn't want to maintained a forked version of a library just to facilitate a unit test.
我一直在尝试模拟 HasSideEffects
超类.我需要在超类中模拟 __init__
和 launch_nukes
以便它们不再产生副作用.我在模拟 __init__
方法时遇到了一些麻烦,显然 Python mock
库不支持它?
I've been trying to mock the HasSideEffects
superclass. I need to mock __init__
and launch_nukes
in the superclass so they no longer perform side effects. I've been having some trouble mocking the __init__
method, apparently the Python mock
library does not support it?
在避免副作用的同时,测试 party
返回 confetti
的最佳方法是什么?
What's the best way to test that party
return confetti
, while avoiding side effects?
推荐答案
也许你已经很接近解决方案了:
Maybe you was quite close to a solution:
我一直在尝试模拟 HasSideEffects 超类.我需要在超类中模拟 init 和 launch_nukes,以便它们不再产生副作用.我在模拟 init 方法时遇到了一些麻烦,显然 Python 模拟库不支持它?
I've been trying to mock the HasSideEffects superclass. I need to mock init and launch_nukes in the superclass so they no longer perform side effects. I've been having some trouble mocking the init method, apparently the Python mock library does not support it?
unittest.mock
和旧的 mock
正是为了完成这种工作而设计的.当您需要从库或资源中删除依赖项时,模拟它并替换为 patch
非常强大,尤其是当您应该使用无法更改的遗留代码时.
unittest.mock
and legacy mock
are designed exactly to do this kind of work. When you need to remove dependencies from library or resources mock it and replace by patch
is powerful especially when you should play with legacy code that you cannot change.
在您的情况下,您需要修补任一 __init__
方法:要做到这一点,您必须考虑两件事
In your case you need to patch either __init__
method: to do it you must take in account two things
- 如果类是子类并且在构建子类对象时需要修补它,则必须直接修补
__init__
而不是类定义. __init__
方法必须返回None
和你的模拟.
- If the class is subclassed and you need patch it when build a subclass object you must patch
__init__
directly instead of class definition. __init__
method MUST returnNone
and your mock too.
现在回到您的简单示例:我对其进行了一些更改以使测试更加明确.
Now come back to your simple example: I changed it a little bit to make tests more explicit.
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()
请注意,装饰器版本简洁明了.
Note as the decorator version is neat and simple.
这篇关于如何模拟作为库一部分的超类?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!