![](/img/trans.png)
[英]How to mock a method inside another method with Python unittest?
[英]How to mock a method that returns self in Python's unittest framework
我正在使用具有方法shuffle
的类,该方法返回调用它的实例的重组版本。 这是:
shuffled_object = unshuffled_object.shuffle(buffer_size)
我想模拟此方法,以便在调用该方法时,它仅返回自身,而没有任何改组。 以下是这种情况的简化:
# my_test.py
class Test():
def shuffle(self, buffer_size):
return self
# test_mock
import unittest
import unittest.mock as mk
import my_test
def mock_test(self, buffer_size):
return self
class TestMock(unittest.TestCase):
def test_mock(self):
with mk.patch('my_test.Test.shuffle') as shuffle:
shuffle.side_effect = mock_test
shuffled_test = my_test.Test().shuffle(5)
但是,当我尝试这样做时,出现以下错误:
TypeError: mock_test() missing 1 required positional argument: 'buffer_size'
仅使用参数5
调用方法,调用实例没有将自身作为方法的self
参数传递。 使用unittest.mock
模块可以实现这种行为吗?
真正的代码如下所示:
# input.py def create_dataset(): ... raw_dataset = tf.data.Dataset.from_generator(data_generator, output_types, output_shapes) shuffled_dataset = raw_dataset.shuffle(buffer_size) dataset = shuffled_dataset.map(_load_example) ... return dataset
# test.py def shuffle(self, buffer_size): return self with mk.patch(input.tf.data.Dataset.shuffle) as shuffle_mock: shuffle_mock.side_effect = shuffle dataset = input.create_dataset()
这里的最大问题是,我只想模拟shuffle
方法,因为在测试时我不希望它是随机的,但是我想保留其余的原始方法,以便我的代码可以继续工作。 最棘手的部分是, shuffle
并不仅仅洗牌调用它的实例,但是它返回的洗后实例,所以我想在测试时,而不是返回数据集的unshuffled版本。
另一方面,使模拟从tf.data.Dataset
继承并不是那么简单,因为据我所知, Dataset
似乎是带有抽象方法的抽象类,我想从自己的任何子类型中抽象自己初始化器from_generator
创建的Dataset
。
通过对方法进行如下修补,使我更进一步:
def shuffle(cls, buffer_size, seed=None, reshuffle_each_iteration=None): def _load_example(example): return example return cls.map(cls, _load_example) from data_input.kitti.kitti_input import tf as tf_mock with mk.patch.object(tf_mock.data.Dataset, 'shuffle', classmethod(shuffle)): dataset = create_dataset()
现在,实例raw_dataset
似乎正在将自身作为shuffle
的self
参数传递,但是无论出于何种原因,代码仍然会由于以下错误而崩溃:
AttributeError: 'property' object has no attribute '_flat_types'
因此,我假设此self
某种程度上与调用实例并不完全相同,它在内部上有所不同。
声明类时,您定义的function
在您的实例中被绑定为method
。 这是它的一个实例:
>>> def function():
... pass
...
>>> type(function)
<class 'function'>
>>> class A:
... def b(self):
... print(self)
>>> type(A.b)
<class 'function'>
>>> a = A()
>>> type(a.b)
<class 'method'>
# So you have the same behavior between the two following calls
>>> A.b(a)
<__main__.A object at 0x7f734511afd0>
>>> a.b()
<__main__.A object at 0x7f734511afd0>
我可以提出一些解决方案,根据您的使用和需求,并非全部引人注目。
您可以模拟整个类以覆盖函数定义。 如前所述,这考虑到您没有使用类的抽象。
import unittest
import unittest.mock as mk
import my_test
import another
class TestMocked(my_test.Test):
def shuffle(self, buffer_size):
return self
@mk.patch("my_test.Test", TestMocked)
# Uncomment to mock the other file behavior
# @mk.patch("another.Test", TestMocked)
def test_mock():
test_class = my_test.Test()
shuffled_test = test_class.shuffle(2)
print(my_test.Test.shuffle)
# This is another file using your class,
# You will have to mock it too in order to see the mocked behavior
print(another.Test.shuffle)
assert shuffled_test == test_class
威奇将输出:
>>> from test_mock import test_mock
>>> test_mock()
<function TestMocked.shuffle at 0x7ff1f03f0ae8>
<function Test.shuffle at 0x7ff1f03f09d8>
我不喜欢这一代码,因为它会使您更改测试代码。 您可以将调用从instance.method()
为class.method(instance)
。 这将按预期将参数发送到您的模拟函数。
# my_input.py
import tensorflow as tf
def data_generator():
for i in itertools.count(1):
yield (i, [1] * i)
def create_dataset():
_load_example = lambda x, y: x+y
buffer_size = 3
output_types = (tf.int64, tf.int64)
output_shapes = (tf.TensorShape([]), tf.TensorShape([None]))
raw_dataset = tf.data.Dataset.from_generator(data_generator, output_types, output_shapes)
shuffled_dataset = tf.data.Dataset.shuffle(raw_dataset, buffer_size)
assert raw_dataset == shuffled_dataset
assert raw_dataset is shuffled_dataset
dataset = shuffled_dataset.map(_load_example)
return dataset
# test_mock.py
import unittest.mock as mk
import my_input
def shuffle(self, buffer_size):
print("Shuffle! {}, {}".format(self, buffer_size))
return self
with mk.patch('my_input.tf.data.Dataset.shuffle') as shuffle_mock:
shuffle_mock.side_effect = shuffle
dataset = my_input.create_dataset()
运行时,将具有以下输出:
$ python test_mock.py
Shuffle! (<DatasetV1Adapter shapes: ((), (?,)), types: (tf.int64, tf.int64)>, 3)
这几乎是前面的答案,但是您可以将其包装为以下内容,而不用从类中调用方法:
# my_input.py
import tensorflow as tf
def data_generator():
for i in itertools.count(1):
yield (i, [1] * i)
def shuffle(instance, buffer_size):
return instance.shuffle(buffer_size)
def create_dataset():
_load_example = lambda x, y: x+y
buffer_size = 3
output_types = (tf.int64, tf.int64)
output_shapes = (tf.TensorShape([]), tf.TensorShape([None]))
raw_dataset = tf.data.Dataset.from_generator(data_generator, output_types, output_shapes)
shuffled_dataset = tf.data.Dataset.shuffle(raw_dataset, buffer_size)
assert raw_dataset == shuffled_dataset
assert raw_dataset is shuffled_dataset
dataset = shuffled_dataset.map(_load_example)
return dataset
# test_mock.py
import unittest.mock as mk
import my_input
def shuffle(self, buffer_size):
print("Shuffle! {}, {}".format(self, buffer_size))
return self
with mk.patch('my_input.shuffle') as shuffle_mock:
shuffle_mock.side_effect = shuffle
dataset = my_input.create_dataset()
我想我已经找到了解决我问题的合理方法。 我没有尝试修补tf.data.Dataset
的shuffle
方法, tf.data.Dataset
认为如果可以访问它,可以直接在要测试的实例上对其进行更改。 因此,我尝试修补创建实例的方法tf.data.Dataset.from_generator
,以便它调用原始方法,但是在返回新创建的实例之前,它将其shuffle
方法替换为另一个简单地返回未tf.data.Dataset.from_generator
的实例的方法。数据集。 该代码将如下所示:
from_generator_old = tf.data.Dataset.from_generator
def from_generator_new(generator, output_types, output_shapes=None, args=None):
dataset = from_generator_old(generator, output_types, output_shapes, args)
dataset.shuffle = lambda *args, **kwargs: dataset
return dataset
from data_input.kitti.kitti_input import tf as tf_mock
with mk.patch.object(tf_mock.data.Dataset, 'from_generator', from_generator_new):
dataset = input.create_dataset()
这似乎可行,但是我不确定这是否正确。 如果有人有更好的主意或可以想到为什么我不应该这样做的原因,则欢迎提出建议或其他答案,但是到目前为止,我认为这是最好的选择。 如果没有人提出更好的建议,我想我会将其标记为可接受的答案。
我已经找到了更好的解决方案。 经过一番阅读之后,我遇到了关于模拟未绑定方法的解释。 显然,当mock.patch.object
与用于autospec
设置为参数True
,修补方法的签名被保持,呼叫引擎盖下的方法的模拟版本。 然后,此方法将绑定到调用它的实例(即,将该实例作为self
参数)。 可以在以下链接下找到说明:
https://het.as.utexas.edu/HET/Software/mock/examples.html#mocking-unbound-methods
在测试时,我还发现,使用tf.test.TestCase
类而不是unittest.TestCase
进行测试时,似乎为整个计算图固定了一个随机种子,因此shuffle
的结果将是每次在此框架下进行测试时都相同。 但是,似乎根本没有记录在案,因此我不确定盲目地依赖它是否是一个好主意。
您在评论中说
我想检查
dataset
是否在迭代时返回正确的元素”。
create_dataset()
客户端不希望这些元素按任何特定顺序排列,只要所有期望的元素并且仅存在期望的元素(无论顺序如何)就可以了。 这就是测试应检查的内容。
def test_create_dataset():
dataset = create_dataset()
assert sorted(dataset) == sorted(expected_elements)
根据迭代数据集时返回的值的类型,断言可能需要更复杂。 例如,如果元素是numpy
数组或pandas.Series
。 在这种情况下,您将需要使用自定义密钥。 这将适用于numpy
和pandas
对象:
sorted(dataset, key=list)
或者您可以使用set
或collections.Counter
...
现在解决评论中表达的一些担忧:
如果您指的是
shuffle
功能
是的,测试想要更改.shuffle()
的实现,并且代码正在尝试隐藏它。 这使得测试难以编写(这就是为什么您必须首先来这里提出问题的原因),并且对于将来的代码维护者而言(很可能包括您将来的自我)很可能难以理解。 我宁愿避免这种情况。
正如我在上面的评论中所说,我认为应该将其替换以使测试更可靠/有意义。
作为create_dataset()
的用户,我不知道,我也不关心改组。 这对我来说毫无意义。 在我调用函数的方式中,没有什么比这更简单,只是实现细节。
让您的测试担心这会使测试变脆,而不是更可靠。 如果将实现更改为不对数据进行Dataset.shuffle()
,或者在不调用Dataset.shuffle()
情况下对数据进行Dataset.shuffle()
,则我仍将获得正确的数据,但测试将失败。 这是为什么? 因为它正在检查我不在乎的东西。 我也会尽量避免这种情况。
毕竟,这不是嘲笑的全部目的吗? 使某些模块的结果可预测,以便隔离您实际要测试的代码的效果?
当然是啦。 好吧,或多或少。 但是要测试的代码(函数create_dataset()
)将改组隐藏在其中,作为实现细节,并与其他行为结合在一起,从调用者的角度来看,这里没有孤立的地方。 现在测试说不,我想调用create_dataset()
但将混洗行为分开,并且没有明显的方法来做,这就是为什么你来这里提出问题。
我宁愿让代码和测试在应将哪些行为相互分离的问题上达成共识,从而避免麻烦。
我宁愿不因为测试而更改代码
也许您应该考虑这样做。 测试可以告诉您一些意料之外的有趣代码用法。 您编写了一个想要更改混洗行为的测试。 其他客户也想这样做也有正当的理由吗? 可重复的研究是一回事,也许将种子作为参数毕竟有意义。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.