如何在 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
似乎将自身作为 shuffle
的 self
参数传递,但无论出于何种原因,代码仍然崩溃并出现以下错误:
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.Dataset
的 shuffle
方法,而是认为如果我有权访问它,我可以直接在要测试的实例上更改它。因此,我尝试修补创建实例的方法 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.object
与 autospec
参数设置为 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
,它将不起作用。在这种情况下,您将需要使用自定义密钥。这适用于 numpy
和 pandas
对象:
sorted(dataset, key=list)
或者您可以使用 set
或 collections.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
或许你应该考虑一下。测试可以告诉您您没有预料到的代码的有趣用途。您编写了一个想要更改洗牌行为的测试。其他客户想要这样做是否有正当理由?可重复的研究是一回事,也许将种子作为参数毕竟有意义。
我使用的 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
似乎将自身作为 shuffle
的 self
参数传递,但无论出于何种原因,代码仍然崩溃并出现以下错误:
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.Dataset
的 shuffle
方法,而是认为如果我有权访问它,我可以直接在要测试的实例上更改它。因此,我尝试修补创建实例的方法 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.object
与 autospec
参数设置为 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
,它将不起作用。在这种情况下,您将需要使用自定义密钥。这适用于 numpy
和 pandas
对象:
sorted(dataset, key=list)
或者您可以使用 set
或 collections.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
或许你应该考虑一下。测试可以告诉您您没有预料到的代码的有趣用途。您编写了一个想要更改洗牌行为的测试。其他客户想要这样做是否有正当理由?可重复的研究是一回事,也许将种子作为参数毕竟有意义。