如何在 Python 的单元测试框架中模拟 returns 自身的方法

How to mock a method that returns self in Python's unittest framework

我使用的 class 有一个方法 shuffle,return 是调用它的实例的随机版本。这是:

shuffled_object = unshuffled_object.shuffle(buffer_size)

我想模拟此方法,以便在调用它时,它 return 只是它自己,没有任何改组。以下是这种情况的简化:

# 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 不只是打乱调用它的实例,而是 return 打乱后的实例,所以我想在测试时 return 而不是数据集的未打乱版本.

另一方面,让 mock 继承自 tf.data.Dataset 并不是那么简单,因为据我了解,Dataset 似乎是一个抽象的 class抽象方法,我想从初始化器 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 参数

当声明 class 时,您定义的 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>

解决方案

我可以提出一些解决方案,根据您的用途和需求,并非所有方案都令人信服。

模拟Class

您可以模拟整个 class 以覆盖函数定义。 如前所述,这考虑到您不使用 class.

的抽象
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

Wich 将输出:

>>> 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)

将方法使用包装在一个函数中

这几乎与之前的答案相同,但不是从 class 调用方法,您可以按如下方式包装它:

# 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.from_generator,以便它调用原始方法,但在返回新创建的实例之前,它用另一个简单地 shuffle 方法替换了 shuffle 方法=38=] 未改变的数据集。代码如下:

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.objectautospec 参数设置为 True 时,修补方法的签名得以保留,在后台调用该方法的模拟版本。然后,此方法将绑定到调用它的实例(即将实例作为 self 参数)。解释可以在下面link:

下找到

https://het.as.utexas.edu/HET/Software/mock/examples.html#mocking-unbound-methods

测试时,我还发现,当使用 tf.test.TestCase class 而不是 unittest.TestCase 进行测试时,随机种子似乎是固定的计算图,所以shuffle的结果在这个框架下每次测试都是一样的。然而,这似乎根本没有记录,所以我不确定盲目依赖它是否是个好主意。

你在评论中说了

I would like to check whether dataset returns the correct elements when iterating over it or not".

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...

现在解决评论中表达的一些问题:

if you mean the shuffle function

是的,测试想要更改 .shuffle() 的实现并且代码试图隐藏它。这使得测试难以编写(这就是为什么你必须首先来这里提出问题)并且很可能难以为代码的未来维护者(可能包括你未来的自己)理解。我宁愿尽量避免它。

as I said in the comments above I think it should be replaced to make the tests more robust/meaningful.

作为 create_dataset() 的用户,我不知道也不关心洗牌。这对我来说毫无意义。我调用函数的方式与此完全不同,它只是一个实现细节。

让你的测试担心这会使测试变得脆弱,而不是更健壮。如果您将实现更改为不打乱数据,或者不调用 Dataset.shuffle() 就打乱数据,我仍然会得到正确的数据,但测试会失败。这是为什么?因为它正在检查我不关心的东西。我也会尽量避免这种情况。

Isn't this, after all, the whole purpose of mocking? Making the outcome of some modules predictable in order to isolate the effects of the code you actually want to test?

当然是。好吧,或多或少。但是您要测试的代码(函数 create_dataset())将洗牌作为实现细节隐藏在其中并与其他行为耦合,从调用者的角度来看,这里没有什么可以隔离的。现在测试说不,我想调用 create_dataset() 但将洗牌行为分开,并且没有明显的方法可以做到这一点,这就是你来这里提问的原因。

我宁愿让代码和测试就哪些行为应该相互分离达成一致,从而避免麻烦。

I would much rather not change my code because of testing

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