繁体   English   中英

如何在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


编辑2:

通过对方法进行如下修补,使我更进一步:

 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似乎正在将自身作为shuffleself参数传递,但是无论出于何种原因,代码仍然会由于以下错误而崩溃:

 AttributeError: 'property' object has no attribute '_flat_types' 

因此,我假设此self某种程度上与调用实例并不完全相同,它在内部上有所不同。

为什么没有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.Datasetshuffle方法, 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 在这种情况下,您将需要使用自定义密钥。 这将适用于numpypandas对象:

sorted(dataset, key=list)

或者您可以使用setcollections.Counter ...

现在解决评论中表达的一些担忧:

如果您指的是shuffle功能

是的,测试想要更改.shuffle()的实现,并且代码正在尝试隐藏它。 这使得测试难以编写(这就是为什么您必须首先来这里提出问题的原因),并且对于将来的代码维护者而言(很可能包括您将来的自我)很可能难以理解。 我宁愿避免这种情况。

正如我在上面的评论中所说,我认为应该将其替换以使测试更可靠/有意义。

作为create_dataset()的用户,我不知道,我也不关心改组。 这对我来说毫无意义。 在我调用函数的方式中,没有什么比这更简单,只是实现细节。

让您的测试担心这会使测试变脆,而不是更可靠。 如果将实现更改为不对数据进行Dataset.shuffle() ,或者在不调用Dataset.shuffle()情况下对数据进行Dataset.shuffle() ,则我仍将获得正确的数据,但测试将失败。 这是为什么? 因为它正在检查我不在乎的东西。 我也会尽量避免这种情况。

毕竟,这不是嘲笑的全部目的吗? 使某些模块的结果可预测,以便隔离您实际要测试的代码的效果?

当然是啦。 好吧,或多或少。 但是要测试的代码(函数create_dataset() )将改组隐藏在其中,作为实现细节,并与其他行为结合在一起,从调用者的角度来看,这里没有孤立的地方。 现在测试说不,我想调用create_dataset()但将混洗行为分开,并且没有明显的方法来做,这就是为什么你来这里提出问题。

我宁愿让代码和测试在应将哪些行为相互分离的问题上达成共识,从而避免麻烦。

我宁愿不因为测试而更改代码

也许您应该考虑这样做。 测试可以告诉您一些意料之外的有趣代码用法。 您编写了一个想要更改混洗行为的测试。 其他客户也想这样做也有正当的理由吗? 可重复的研究是一回事,也许将种子作为参数毕竟有意义。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM