我正在寻找一种在单元测试中正确模拟对象的方法,但是我无法让unittest.mock.create_autospec
或unittest.mock.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
替换为Mock对象,我希望能够确定我的单元测试是否在以下位置运行任何代码:
FileStorageClient
或FileStorageBucket
上都不存在的方法被调用FileStorageClient
或FileStorageBucket
上确实存在的方法使用错误的参数调用因此,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中手动设置“ 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实例不同,使用Mock(spec=whatever)
的Mock实例似乎没有检查是否将正确的参数传递给了函数:
>>> client.auth(wrong_arg=1)
<__main__.FileStorageClient at 0x10dac5990>
只需将return_value
方法上的get_bucket
设置为具有不同规格的另一个模拟即可。您无需弄乱创建MockStorageBucket
和MockStorageClient
。
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")
我认为我想要的完整代码如下:
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