我如何对依赖urllib2的模块进行单元测试?

27 投票
7 回答
9363 浏览
提问于 2025-04-15 19:22

我有一段代码,不知道怎么进行单元测试!这个模块是用来从外部的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 个回答

6

建立一个单独的类或模块,专门负责与外部数据源进行沟通。

让这个类能够作为一个测试替身。你在用Python,所以这方面比较简单;如果你用的是C#,我会建议使用接口或虚方法。

在你的单元测试中,插入一个外部数据源类的测试替身。测试你的代码是否正确使用了这个类,假设这个类能够正确地与外部资源沟通。让你的测试替身返回假数据,而不是实时数据;测试各种数据组合,以及urllib2可能抛出的各种异常。

就这样。

你无法有效地自动化依赖外部来源的单元测试,所以最好是不要这样做。偶尔对你的通信模块进行集成测试,但不要把这些测试作为自动化测试的一部分。

编辑:

只是想说明一下我和@Crast的回答之间的区别。两者本质上都是正确的,但方法不同。在Crast的方法中,你直接在库上使用测试替身。而在我的方法中,你将库的使用抽象到一个单独的模块中,并对这个模块进行测试替身。

你选择哪种方法完全是个人偏好;没有“正确”的答案。我更喜欢我的方法,因为它让我能写出更模块化、更灵活的代码,这是我所重视的。但这也意味着需要写更多的代码,在很多敏捷开发的情况下,这可能并不被看重。

9

最好是你能写一个模拟的 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。

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

撰写回答