关于python:针对Django单元测试的远程模拟远程服务器和API

Cleanly Mocking Remote Servers and APIs for Django Unittests

我有一个棘手的问题,我似乎无法解决。我是
目前正在为django自定义auth-backend编写单元测试。在我们的
系统我们实际上有两个后端:一个内置的django后端
以及将请求发送到基于Java的API的自定义后端
以XML形式返回用户信息。现在,我正在写单元
测试,所以我不想像系统外发送请求
那,我不是要测试Java API,所以我的问题是如何
解决这个问题,并以最可靠的方式模拟副作用。

我正在测试的功能是这样的,其中网址
设置值只是Java服务器的基本网址,
验证用户名和密码数据并返回xml,服务值为
只是一些用于构建url查询的魔术,对于
我们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@staticmethod
def get_info_from_api_with_un_pw(username, password, service=12345):
    url = settings.AUTHENTICATE_URL_VIA_PASSWORD
    if AUTH_FIELD =="username":
        params = {"nick": username,"password": password}
    elif AUTH_FIELD =="email":
        params = {"email": username,"password": password}
    params["service"] = service
    encoded_params = urlencode([(k, smart_str(v,"latin1")) for k, v in params.items()])
    try:
        # get the user's data from the api
        xml = urlopen(url + encoded_params).read()
        userinfo = dict((e.tag, smart_unicode(e.text, strings_only=True))
                        for e in ET.fromstring(xml).getchildren())
        if"nil" in userinfo:
            return userinfo
        else:
            return None

因此,我们获取了xml,将其解析为dict,如果键nil存在,
那么我们就可以返回字典并继续进行快乐和认证了。
显然,一种解决方案是找到一种方法以某种方式覆盖或
Monkeypatch在xml变量中的逻辑,我找到了这个答案:

像urllib这样的模拟/存根python模块如何

我试图实现类似的方法,但是细节
非常粗略,我似乎无法正常工作。

我还捕获了xml响应,并将其放在本地的文件中
测试文件夹,旨在找到一种将其用作模拟的方法
传递给测试函数的url参数的响应,
像这样的东西会覆盖URL:

1
2
3
4
@override_settings(AUTHENTICATE_URL_VIA_PASSWORD=(os.path.join(os.path.dirname(__file__),"{0}".format("response.xml"))))
def test_get_user_info_username(self):
    self.backend = RemoteAuthBackend()
    self.backend.get_info_from_api_with_un_pw("user","pass")

但这还需要考虑网址构建逻辑,即
函数定义(即" url + encode_params")。同样,我可以重命名
响应文件与串联的url相同,但这正在成为
不太像是对功能进行良好的单元测试,而更像是"作弊"
这些解决方案使事情变得越来越脆弱,无论如何它实际上只是一个固定装置,如果要避免的话,这也是我要避免的事情
在所有可能的情况下。

我还想知道是否有办法在django开发服务器上提供xml,然后将功能指向该位置?看来这是一个更明智的解决方案,但是如果有这样的事情是可能的或可取的,那么很多谷歌搜索都无法给我提供任何线索,即使如此,我也不认为这是在开发环境之外运行的测试。

因此,理想情况下,我需要能够以某种方式模拟"服务器"
在函数调用中代替Java API,或以某种方式提供服务
可以将该函数作为其URL打开的一些xml有效负载,或者
从测试本身中提取功能,或者...

模拟库是否具有执行此类操作的适当工具?

http://www.voidspace.org.uk/python/mock

因此,这个问题有两点:1)我想解决我的问题
干净地解决特定问题,更重要的是2)什么是
编写干净的Django单元测试的最佳实践
依赖于数据,cookie等,以从远程进行用户身份验证
您网域之外的API?


如果使用正确,模拟库应该可以工作。 我更喜欢minimock库,并编写了一个小型基本单元测试用例(minimocktest)来帮助实现这一点。

如果要将此测试用例与Django集成以测试urllib,则可以按以下步骤进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from minimocktest import MockTestCase
from django.test import TestCase
from django.test.client import Client

class DjangoTestCase(TestCase, MockTestCase):
    '''
    A TestCase class that combines minimocktest and django.test.TestCase
    '''


    def _pre_setup(self):
        MockTestCase.setUp(self)
        TestCase._pre_setup(self)
        # optional: shortcut client handle for quick testing
        self.client = Client()

    def _post_teardown(self):
        TestCase._post_teardown(self)
        MockTestCase.tearDown(self)

现在,您可以使用此测试用例,而不是直接使用Django测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MySimpleTestCase(DjangoTestCase):
    def setUp(self):
        self.file = StringIO.StringIO('MiniMockTest')
        self.file.close = self.Mock('file_close_function')
    def test_urldump_dumpsContentProperly(self):
        self.mock('urllib2.urlopen', returns=self.file)
        self.assertEquals(urldump('http://pykler.github.com'), 'MiniMockTest')
        self.assertSameTrace('
'
.join([
           "Called urllib2.urlopen('http://pykler.github.com')",
           "Called file_close_function()",
        ]))
        urllib2.urlopen('anything')
        self.mock('urllib2.urlopen', returns=self.file, tracker=None)
        urllib2.urlopen('this is not tracked')
        self.assertTrace("Called urllib2.urlopen('anything')")
        self.assertTrace("Called urllib2.urlopen('this is mocked but not tracked')", includes=False)
        self.assertSameTrace('
'
.join([
           "Called urllib2.urlopen('http://pykler.github.com')",
           "Called file_close_function()",
           "Called urllib2.urlopen('anything')",
        ]))


这是我最终记录的解决方案的基础。 最后,我使用了Mock库本身而不是Mockito,但是想法是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
from mock import patch

@override_settings(AUTHENTICATE_LOGIN_FIELD="username")
@patch("mymodule.auth_backend.urlopen")
def test_get_user_info_username(self, urlopen_override):
    response ="file://" + os.path.join(os.path.dirname(__file__),"{0}".format("response.xml"))
    # mock patch replaces API call
    urlopen_override.return_value = urlopen(response)
    # call the patched object
    userinfo = RemoteAuthBackend.get_info_from_api_with_un_pw("user","pass")
    assert_equal(type(userinfo), dict)
    assert_equal(userinfo["nick"],"user")
    assert_equal(userinfo["pass"],"pass")