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集成以测试
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") |