为了账号安全,请及时绑定邮箱和手机立即绑定

如何在 Python 的 unittest 框架中模拟返回 self 的方法

如何在 Python 的 unittest 框架中模拟返回 self 的方法

慕容森 2022-05-24 16:29:50
我正在使用一个类,该类具有一个方法,该方法shuffle返回调用它的实例的洗牌版本。这是:shuffled_object = unshuffled_object.shuffle(buffer_size)我想模拟这个方法,这样当它被调用时,它会简单地返回自身,而不需要任何改组。以下将是这种情况的简化:# my_test.pyclass Test():    def shuffle(self, buffer_size):        return self# test_mockimport unittestimport unittest.mock as mkimport my_testdef mock_test(self, buffer_size):    return selfclass 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.pydef 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.pydef shuffle(self, buffer_size):    return selfwith mk.patch(input.tf.data.Dataset.shuffle) as shuffle_mock:    shuffle_mock.side_effect = shuffle    dataset = input.create_dataset()这里最大的问题是我只想模拟该shuffle方法,因为我不希望它在测试时是随机的,但我想保留其余的原始方法,以便我的代码可以继续工作。棘手的部分是,shuffle它不仅打乱调用它的实例,而且返回打乱的实例,所以我想在测试时返回数据集的未打乱版本。另一方面,让 mock 继承并不是那么简单,tf.data.Dataset因为据我了解,它Dataset似乎是一个带有抽象方法的抽象类,我想从Dataset初始化程序from_generator创建的任何子类型中抽象出自己。
查看完整描述

3 回答

?
侃侃尔雅

TA贡献1801条经验 获得超16个赞

为什么你没有 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()


查看完整回答
反对 回复 2022-05-24
?
莫回无

TA贡献1865条经验 获得超7个赞

我想我已经找到了解决问题的合理方法。我没有尝试修补 的shuffle方法tf.data.Dataset,而是认为如果我可以访问它,我可以直接在要测试的实例上更改它。因此,我尝试修补创建实例的方法tf.data.Dataset.from_generator,以便它调用原始方法,但在返回新创建的实例之前,它用shuffle另一个简单地返回未更改数据集的方法替换它的方法。代码如下:


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每次在这个框架下测试的结果都是一样的。然而,这似乎根本没有记录在案,所以我不确定盲目依赖它是否是个好主意。


查看完整回答
反对 回复 2022-05-24
?
暮色呼如

TA贡献1853条经验 获得超9个赞

你在评论中说

我想检查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(),我仍然会得到正确的数据,但测试会失败。这是为什么?因为它正在检查我不关心的东西。我也会尽量避免这种情况。

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

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

我宁愿通过让代码和测试就应该将哪些行为相互解耦达成一致,从而省去这些麻烦。

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

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


查看完整回答
反对 回复 2022-05-24
  • 3 回答
  • 0 关注
  • 135 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号