我有一段代码,我无法弄清楚如何进行单元测试!该模块使用urllib2从外部XML提要(twitter,flickr,youtube等)中提取内容。这是一些伪代码:
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并不理想,因为我的代码也使用了头信息。它旨在作用于响应对象。
当然,在单元测试中依赖外部数据源是一个可怕的想法。
那么如何为此编写单元测试?
答案 0 :(得分:25)
urllib2有一个名为build_opener()
和install_opener()
的函数,您应该使用这些函数来模拟urlopen()
的行为
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
答案 1 :(得分:9)
最好是你可以编写一个mock urlopen(可能还有Request),它提供了所需的最小接口,就像urllib2的版本一样。然后你需要让你的函数/方法使用它能够以某种方式接受这个模拟urlopen,否则使用urllib2.urlopen
。
这是相当多的工作,但值得。请记住,python对ducktyping非常友好,所以你只需要提供一些响应对象的属性来模拟它。
例如:
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,它实现了像case-insensitive标题名称这样的有趣的东西。但是,您可以使用响应数据简单地构造HTTPMessage。
答案 2 :(得分:6)
构建一个单独的类或模块,负责与外部供稿进行通信。
使此课程成为test double。你正在使用python,所以你在那里很漂亮;如果您使用的是C#,我建议使用接口或虚拟方法。
在单元测试中,插入外部Feed类的测试双。测试您的代码是否正确使用该类,假设该类正确地执行与外部资源通信的工作。让你的测试双重返回假数据而不是实时数据;测试数据的各种组合,当然还有urllib2可能抛出的异常。
Aand ......就是这样。
您无法有效地自动化依赖外部源的单元测试,因此您最好不要不这样做。在您的通信模块上运行偶尔的集成测试,但不要将这些测试作为自动测试的一部分。
修改:
关于我的回答和@Crast的回答之间的区别。两者基本上都是正确的,但它们涉及不同的方法。在Crast的方法中,您在库本身上使用了测试双精度。在我的方法中,您将库的使用抽象为一个单独的模块并测试该模块的两倍。
您使用哪种方法完全是主观的;那里没有“正确”的答案。我更喜欢我的方法,因为它允许我构建更多模块化,灵活的代码,这是我重视的。但是在编写额外代码方面需要付出代价,这在某些敏捷情况下可能无法得到重视。
答案 3 :(得分:5)
您可以使用pymox来模拟urllib2(或任何其他)包中的任何内容和所有内容的行为。这是2010年,你不应该写自己的模拟课程。
答案 4 :(得分:1)
我认为最简单的方法是在单元测试中实际创建一个简单的Web服务器。当您开始测试时,创建一个侦听某个任意端口的新线程,当客户端连接时只返回一组已知的头和XML,然后终止。
如果您需要更多信息,我可以详细说明。
以下是一些代码:
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())
要运行单元测试,请致电serve_data()
,然后致电请求代码为http://localhost:12345/anythingyouwant
的网址的代码。
答案 5 :(得分:0)
为什么不只是mock a website会返回您期望的响应?然后在设置中的线程中启动服务器并在拆解中将其杀死。我最终这样做是为了测试通过模拟smtp服务器发送电子邮件的代码,它运行良好。当然可以为http ...
做一些更微不足道的事情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"
答案 6 :(得分:0)
试着改善一下@ john-la-rooy的回答,我做了一个允许简单模拟单元测试的小课程
应该使用python 2和3
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)
像这样使用:
class TestOther(unittest.TestCase):
def setUp(self):
previous = MockHTTPHandler.install()
self.addCleanup(MockHTTPHandler.remove, previous)