可以模拟自动实例化模拟对象吗?

时间:2014-08-14 09:50:03

标签: python unit-testing mocking

我正在为类OnlineService编写测试,它实例化一个类型为api.API的类,后者又实例化一个类型为api.Resource的类。我在此示例中测试的方法是initialize,它通过向远程API中的GET资源发出Ping请求来测试与远程服务的连接。

我目前正在使用以下代码将这些对象修补为mocks,但它看起来仍然有些冗长。

@patch('api.Resource')
@patch('api.API')
def test_initialize(self, api_mock, resource_mock):
    api_instance = api_mock.return_value
    api_instance.Ping = resource_mock.return_value # is this step really necessary?
    api_instance.Ping.get.side_effect = [None, HTTPError()]

    service = OnlineService()

    service.initialize()
    assert service.connected is True

    service.initialize()
    assert service.connected is False

我是否真的必须手动将Resource模拟实例分配给另一个模拟实例的属性?也许mock包中有一些功能可以为我做这个?

更新

我将测试分成两部分,并附加了正在测试的OnlineService的相关代码。这是OnlineService类:

class OnlineService(object):
    def __init__(self):
        self.webservice_url = u''
        self.verify_ssl = True
        self.connected = False

    def initialize(self, webservice_url, verify_ssl, connectivity_check_timeout):
        self.webservice_url = webservice_url
        self.verify_ssl = verify_ssl
        self.connected = self.can_connect_to_api(connectivity_check_timeout)

    def can_connect_to_api(self, connectivity_check_timeout):
        api_instance = api.API(url=self.webservice_url, verify_ssl=self.verify_ssl, timeout=connectivity_check_timeout)
        try:
            # api_instance.Ping of type api.Resource was instantiated in api.API()
            api_instance.Ping.get()
            return True
        except:
            return False

这是测试代码:

def test_initialize(self):
    service = OnlineService()
    service.can_connect_to_api = MagicMock(return_value=True)

    service.initialize(u'some_url', False, 3.42)

    service.can_connect_to_api.assert_called_once_with(3.42)
    assert service.webservice_url is u'some_url'
    assert service.verify_ssl is False
    assert service.connected is True

@patch('api.Resource')
@patch('api.API')
def test_can_connect_to_api(self, api_mock, resource_mock):
    api_instance = api_mock.return_value
    api_instance.Ping = resource_mock.return_value # is this step really necessary?
    api_instance.Ping.get.side_effect = [None, HTTPError()]

    service = OnlineService()

    connected = service.can_connect_to_api(5.0)
    assert connected is True

    connected = service.can_connect_to_api(5.0)
    assert connected is False

目前测试通过,如果我运行它。评论我们正在讨论的那条线路给了我test_can_connect_to_api中的以下失败:

======================================================================
FAIL: Services.tests.test_OnlineService.TestOnlineService.test_can_connect_to_api
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Development\Projects\app\venv\lib\site-packages\nose\case.py", line 197, in runTest
    self.test(*self.arg)
  File "C:\Development\Projects\app\venv\lib\site-packages\mock.py", line 1201, in patched
    return func(*args, **keywargs)
  File "C:\Development\Projects\app\src\Services\tests\test_OnlineService.py", line 47, in test_can_connect_to_api
    assert connected is False
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.013s

FAILED (failures=1)

1 个答案:

答案 0 :(得分:1)

该行:

api_instance.Ping = resource_mock.return_value # is this step really necessary?

将新的MagicMock实例分配给api_instance.Ping。但是,在没有分配已经的情况下访问Ping会这样做,因为api_instance本身就是MagicMock个对象;这条线完全是多余的:

>>> from unittest.mock import MagicMock
>>> api_instance = MagicMock()
>>> api_instance.Ping
<MagicMock name='mock.Ping' id='4515362016'>

因此,以下就足够了:

@patch('api.API')
def test_initialize(self, api_mock):
    api_instance = api_mock.return_value
    api_instance.Ping.get.side_effect = [None, HTTPError()]

当然,如果被测代码使用api.API().Ping.get来获取资源,那么上面的代码将无法实现其目标;但是你不需要改变api_instance.Ping

这里要记住的是你替换了api.API ;那个班级原作所做的不再是你所关心的。您需要做的就是使用 api.API来管理代码的期望;如果它使用api.API()并在该对象上使用属性或方法,那就嘲笑它们。如果被测代码未直接使用api.Resource,请将其从测试中删除。

您添加的代码显示您模拟了错误的对象。您正确地模拟了api.Resource,但CUT中的API()对象不是模拟。请参阅unittest.mock文档的Where to patch section。您的CUT使用全局名称API;它确实引用api.API。模拟全局

@patch('module_under_test.API')
def test_initialize(self, api_mock):
    api_instance = api_mock.return_value
    api_instance.Ping.get.side_effect = [None, HTTPError()]

或者你可以嘲笑只是 Ping资源;显然,这就是你的未被嘲弄的API()课使用的,毕竟:

@patch('api.Resource')
def test_can_connect_to_api(self, resource_mock):
    # API().Ping is an instance of api.Resource; mocking that also works
    resource_mock.return_value.get.side_effect = [None, HTTPError()]