如何在Python中将继承与模拟autospec相结合

2024-04-24 14:45:16 发布

您现在位置:Python中文网/ 问答频道 /正文

问题

我正在寻找一种在单元测试中正确模拟对象的方法,但是我在让unittest.mock.create_autospecunittest.mock.Mock执行我需要的操作时遇到了困难。我想我需要对模拟对象使用继承,但是我在使它工作时遇到了困难

我需要模拟第三方模块中的类的图像,看起来像这样(假设带有raise NotImplementedError的行是我希望在单元测试中避免的外部API调用):

class FileStorageBucket():
    def __init__(self, bucketname: str) -> None:
        self.bucketname = bucketname

    def download(self, filename) -> None:
        raise NotImplementedError

    # ...lots more methods...


class FileStorageClient():
    def auth(self, username: str, password: str) -> None:
        raise NotImplementedError

    def get_bucket(self, bucketname: str) -> FileStorageBucket:
        raise NotImplementedError
        return FileStorageBucket(bucketname)

    # ...lots more methods...

它可能在我的应用程序中的其他地方使用,如下所示:

client = FileStorageClient()
client.auth("me", "mypassword")
client.get_bucket("my-bucket").download("my-file.jpg")

如果我用模拟对象替换FileStorageClient,我希望能够找出我的单元测试是否运行以下代码:

  • 调用FileStorageClientFileStorageBucket上不存在的方法
  • FileStorageClientFileStorageBucket上确实存在的方法使用错误的参数调用

因此,client.get_bucket("foo").download()应该引发一个异常,即filename是.download()的必需参数

我尝试过的事情:

首先,我尝试使用create_autospec。它能够捕获某些类型的错误:

>>> MockClient = create_autospec(FileStorageClient)
>>> client = MockClient()
>>> client.auth(user_name="name", password="password")
TypeError: missing a required argument: 'username'

但是,当然,因为它不知道get_bucket应该具有的返回类型,所以它不会捕获其他类型的错误:

>>> MockClient = create_autospec(FileStorageClient)
>>> client = MockClient()
>>> client.get_bucket("foo").download(wrong_arg="foo")
<MagicMock name='mock.get_bucket().download()' id='4554265424'>

我想我可以通过创建继承自create_autospec输出的类来解决这个问题:

class MockStorageBucket(create_autospec(FileStorageBucket)):
    def path(self, filename) -> str:
        return f"/{self.bucketname}/{filename}"


class MockStorageClient(create_autospec(FileStorageClient)):
    def get_bucket(self, bucketname: str):
        bucket = MockStorageBucket()
        bucket.bucketname = bucketname
        return bucket

但它实际上并没有像预期的那样返回MockStorageBucket实例:

>>> client = MockStorageClient()
>>> client.get_bucket("foo").download(wrong_arg="foo")
<MagicMock name='mock.get_bucket().download()' id='4554265424'>

因此,我尝试从Mock继承并在init中手动设置“spec”:

class MockStorageBucket(Mock):
    def __init__(self, *args, **kwargs):
        # Pass `FileStorageBucket` as the "spec"
        super().__init__(FileStorageBucket, *args, **kwargs)

    def path(self, filename) -> str:
        return f"/{self.bucketname}/{filename}"


class MockStorageClient(Mock):
    def __init__(self, *args, **kwargs):
        # Pass `FileStorageClient` as the "spec"
        super().__init__(FileStorageClient, *args, **kwargs)

    def get_bucket(self, bucketname: str):
        bucket = MockStorageBucket()
        bucket.bucketname = bucketname
        return bucket

现在,get_bucket方法按预期返回一个MockStorageBucket实例,我能够捕获一些错误,例如访问不存在的属性:

>>> client = MockStorageClient()
>>> client.get_bucket("my-bucket")
<__main__.FileStorageBucket at 0x10f7a0110>
>>> client.get_bucket("my-bucket").foobar
AttributeError: Mock object has no attribute 'foobar'

但是,与使用create_autospec创建的模拟实例不同,使用Mock(spec=whatever)创建的模拟实例似乎不会检查是否将正确的参数传递给函数:

>>> client.auth(wrong_arg=1)
<__main__.FileStorageClient at 0x10dac5990>

Tags: selfclientgetbucketinitdownloaddefcreate
2条回答

我想我想要的完整代码是这样的:

def mock_client_factory() -> Mock:
    MockClient = create_autospec(FileStorageClient)

    def mock_bucket_factory(bucketname: str) -> Mock:
        MockBucket = create_autospec(FileStorageBucket)
        mock_bucket = MockBucket(bucketname=bucketname)
        mock_bucket.bucketname = bucketname
        return mock_bucket

    mock_client = MockClient()
    mock_client.get_bucket.side_effect = mock_bucket_factory
    return mock_client

只需将get_bucket方法上的return_value设置为另一个具有不同规范的模拟。您不需要乱创建MockStorageBucketMockStorageClient

mock_client = create_autospec(FileStorageClient, spec_set=True)
mock_bucket = create_autospec(FileStorageBucket, spec_set=True)
mock_client.get_bucket.return_value = mock_bucket

mock_client.get_bucket("my-bucket").download("my-file.jpg")

相关问题 更多 >