【问题标题】:How to combine inheritance with mock autospec in Python如何在 Python 中将继承与模拟 autospec 结合起来
【发布时间】:2020-01-30 00:54:00
【问题描述】:

问题

我正在寻找一种在我的单元测试中正确模拟对象的方法,但我无法让 unittest.mock.create_autospecunittest.mock.Mock 做我需要的事情。我想我需要对 Mock 对象使用继承,但我无法让它工作。

我需要在看起来像这样的第 3 方模块中模拟类的图像(假设带有 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 替换为 Mock 对象,我希望能够确定我的单元测试是否在以下位置运行任何代码:

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

因此,client.get_bucket("foo").download() 应该引发一个异常,即文件名是 .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 中手动设置“规范”:

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 实例不同,使用 Mock(spec=whatever) 的 Mock 实例似乎不会检查是否将正确的参数传递给函数:

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

【问题讨论】:

    标签: python unit-testing mocking python-unittest python-mock


    【解决方案1】:

    只需在您的 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")
    

    【讨论】:

    • 迄今为止最好的解决方案,我在继承尝试中缺少的一件事是 client.get_bucket("my-bucket") 调用将其存储桶名称传递给返回的 mock_bucket 实例。
    • 我可能会建议这可能会让事情变得有点过于接近tautological test
    • 我不认为这属于重言式测试类别,因为我不会用这段代码来测试FileStorageClient,我会用它来测试依赖FileStorageClient的代码
    【解决方案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
    

    【讨论】:

      猜你喜欢
      • 2011-12-10
      • 2011-08-22
      • 2023-03-28
      • 2012-01-30
      • 2021-11-27
      • 1970-01-01
      • 1970-01-01
      • 2018-01-15
      • 2020-01-23
      相关资源
      最近更新 更多