我如何对依赖urllib2的模块进行单元测试?
我有一段代码,不知道怎么进行单元测试!这个模块是用来从外部的XML数据源(比如twitter、flickr、youtube等)获取内容的,使用的是urllib2。下面是一些伪代码:
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内容也不太合适,因为我的代码还需要用到头部信息。它是设计来处理响应对象的。
当然,在单元测试中依赖外部数据源是个糟糕的主意。
那么,我该怎么为这个写单元测试呢?
7 个回答
建立一个单独的类或模块,专门负责与外部数据源进行沟通。
让这个类能够作为一个测试替身。你在用Python,所以这方面比较简单;如果你用的是C#,我会建议使用接口或虚方法。
在你的单元测试中,插入一个外部数据源类的测试替身。测试你的代码是否正确使用了这个类,假设这个类能够正确地与外部资源沟通。让你的测试替身返回假数据,而不是实时数据;测试各种数据组合,以及urllib2可能抛出的各种异常。
就这样。
你无法有效地自动化依赖外部来源的单元测试,所以最好是不要这样做。偶尔对你的通信模块进行集成测试,但不要把这些测试作为自动化测试的一部分。
编辑:
只是想说明一下我和@Crast的回答之间的区别。两者本质上都是正确的,但方法不同。在Crast的方法中,你直接在库上使用测试替身。而在我的方法中,你将库的使用抽象到一个单独的模块中,并对这个模块进行测试替身。
你选择哪种方法完全是个人偏好;没有“正确”的答案。我更喜欢我的方法,因为它让我能写出更模块化、更灵活的代码,这是我所重视的。但这也意味着需要写更多的代码,在很多敏捷开发的情况下,这可能并不被看重。
最好是你能写一个模拟的 urlopen(可能还需要 Request),这个模拟的东西要能提供最基本的接口,跟 urllib2 的版本表现得差不多。然后,你的函数或者方法需要能够接受这个模拟的 urlopen,或者在其他情况下使用 urllib2.urlopen
。
这工作量不小,但很值得。记住,Python 对鸭子类型(duck typing)非常友好,所以你只需要提供一些响应对象的属性,让它看起来像真的就行。
举个例子:
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>')
当然,有些东西比较难模拟,比如我觉得正常的“headers”是一个 HTTPMessage,它实现了一些有趣的功能,比如不区分大小写的头部名称。不过,你可能可以简单地用你的响应数据构造一个 HTTPMessage。
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