How do I unit test a module that relies on urllib2?
我有一段代码,我不知道如何进行单元测试! 该模块使用urllib2从外部XML提要(twitter,flickr,youtube等)中提取内容。 这是一些伪代码:
1 2 3 4 5 | params = (url, urlencode(data),) if data else (url,) req = Request(*params) response = urlopen(req) #check headers, content-length, etc... #parse the response XML with lxml... |
我的第一个想法是腌制响应并加载它以进行测试,但是显然urllib的响应对象是不可序列化的(它引发异常)。
仅从响应正文中保存XML是不理想的,因为我的代码也使用标头信息。 它旨在作用于响应对象。
当然,在单元测试中依赖外部数据源是一个可怕的想法。
那么,如何为此编写单元测试?
urllib2有一个名为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import urllib2 from StringIO import StringIO def mock_response(req): if req.get_full_url() =="http://example.com": resp = urllib2.addinfourl(StringIO("mock file"),"mock message", req.get_full_url()) resp.code = 200 resp.msg ="OK" return resp class MyHTTPHandler(urllib2.HTTPHandler): def http_open(self, req): print"mock opener" return mock_response(req) my_opener = urllib2.build_opener(MyHTTPHandler) urllib2.install_opener(my_opener) response=urllib2.urlopen("http://example.com") print response.read() print response.code print response.msg |
最好是编写一个模拟的urlopen(可能还有Request),它提供行为所需的最低要求,类似于urllib2的版本。然后,您需要具有使用它的函数/方法,使其能够以某种方式接受此模拟urlopen,否则使用
这是相当多的工作,但值得。请记住,python非常适合小鸭子输入,因此您只需要提供响应对象属性的某种外观即可对其进行模拟。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class MockResponse(object): def __init__(self, resp_data, code=200, msg='OK'): self.resp_data = resp_data self.code = code self.msg = msg self.headers = {'content-type': 'text/xml; charset=utf-8'} def read(self): return self.resp_data def getcode(self): return self.code # Define other members and properties you want def mock_urlopen(request): return MockResponse(r'<xml document>') |
当然,其中一些很难模拟,因为例如,我相信普通的"标头"是HTTPMessage,它实现了一些有趣的功能,例如不区分大小写的标头名称。但是,您也许可以简单地用响应数据构造一个HTTPMessage。
构建一个单独的类或模块,负责与您的外部供稿进行通信。
使该课程成为测试的两倍。您使用的是python,因此在那里相当漂亮。如果您使用的是C#,建议您使用接口或虚拟方法。
在单元测试中,插入外部提要类的测试双。假设该类可以正确地与您的外部资源进行通信,请测试您的代码是否正确使用了该类。让您的测试重复返回假数据而不是实时数据;测试数据的各种组合,当然还要测试urllib2可能引发的异常。
阿安德...就是这样。
您不能有效地自动化依赖外部资源的单元测试,因此最好不要这样做。在您的通信模块上偶尔运行集成测试,但不要将这些测试包括在自动化测试中。
编辑:
请注意我的答案和@Crast答案之间的区别。两者本质上都是正确的,但是它们涉及不同的方法。在Crast的方法中,您对库本身使用了double测试。在我的方法中,您将库的使用抽象为一个单独的模块,然后对该模块进行两次测试。
您使用哪种方法完全是主观的;那里没有"正确"的答案。我更喜欢我的方法,因为它使我能够构建更有价值的模块化,灵活的代码。但这要付出额外编写代码的代价,这在许多敏捷情况下可能都不值一提。
您可以使用pymox模拟urllib2(或任何其他)程序包中任何东西的行为。在2010年,您不应该编写自己的模拟类。
我认为最简单的方法是在单元测试中实际创建一个简单的Web服务器。开始测试时,创建一个新线程,该线程在任意端口上侦听,并且当客户端连接时,它仅返回一组已知的头和XML,然后终止。
如果您需要更多信息,我可以详细说明。
这是一些代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import threading, SocketServer, time # a request handler class SimpleRequestHandler(SocketServer.BaseRequestHandler): def handle(self): data = self.request.recv(102400) # token receive senddata = file(self.server.datafile).read() # read data from unit test file self.request.send(senddata) time.sleep(0.1) # make sure it finishes receiving request before closing self.request.close() def serve_data(datafile): server = SocketServer.TCPServer(('127.0.0.1', 12345), SimpleRequestHandler) server.datafile = datafile http_server_thread = threading.Thread(target=server.handle_request()) |
要运行单元测试,请调用
尝试改善@ john-la-rooy答案,我制作了一个小类,允许对单元测试进行简单的模拟
应该与python 2和3一起工作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | try: import urllib.request as urllib except ImportError: import urllib2 as urllib from io import BytesIO class MockHTTPHandler(urllib.HTTPHandler): def mock_response(self, req): url = req.get_full_url() print("incomming request:", url) if url.endswith('.json'): resdata = b'[{"hello":"world"}]' headers = {'Content-Type': 'application/json'} resp = urllib.addinfourl(BytesIO(resdata), header, url, 200) resp.msg ="OK" return resp raise RuntimeError('Unhandled URL', url) http_open = mock_response @classmethod def install(cls): previous = urllib._opener urllib.install_opener(urllib.build_opener(cls)) return previous @classmethod def remove(cls, previous=None): urllib.install_opener(previous) |
像这样使用:
1 2 3 4 5 | class TestOther(unittest.TestCase): def setUp(self): previous = MockHTTPHandler.install() self.addCleanup(MockHTTPHandler.remove, previous) |
为什么不仅仅模拟一个返回您期望的响应的网站?然后在安装程序中的线程中启动服务器,并在拆卸中将其杀死。我最终这样做是为了测试将通过模拟smtp服务器发送电子邮件的代码,并且效果很好。当然,对于http ...可以做些更简单的事情...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | from smtpd import SMTPServer from time import sleep import asyncore SMTP_PORT = 6544 class MockSMTPServer(SMTPServer): def __init__(self, localaddr, remoteaddr, cb = None): self.cb = cb SMTPServer.__init__(self, localaddr, remoteaddr) def process_message(self, peer, mailfrom, rcpttos, data): print (peer, mailfrom, rcpttos, data) if self.cb: self.cb(peer, mailfrom, rcpttos, data) self.close() def start_smtp(cb, port=SMTP_PORT): def smtp_thread(): _smtp = MockSMTPServer(("127.0.0.1", port), (None, 0), cb) asyncore.loop() return Thread(None, smtp_thread) def test_stuff(): #.......snip noise email_result = None def email_back(*args): email_result = args t = start_smtp(email_back) t.start() sleep(1) res.form["email"]= self.admin_email res = res.form.submit() assert res.status_int == 302,"should've redirected" sleep(1) assert email_result is not None,"didn't get an email" |