【问题标题】:How do I unit test a module that relies on urllib2?如何对依赖 urllib2 的模块进行单元测试?
【发布时间】:2010-02-16 22:02:16
【问题描述】:

我有一段代码不知道如何进行单元测试!该模块使用 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 并不理想,因为我的代码也使用了标头信息。它旨在作用于响应对象。

当然,在单元测试中依赖外部数据源是一个可怕的想法。

那么我该如何为此编写单元测试呢?

【问题讨论】:

标签: python unit-testing urllib2 urllib


【解决方案1】:

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

【讨论】:

  • 这很酷,我实际上不知道 urllib2 允许您安装备用开启程序。我看到的唯一问题是,这意味着您已经更改了全局共享状态,这意味着对 urllib2.urlopen 的任何后续调用都将使用您的处理程序,除非您重新注册旧的处理程序(如果您正在运行单个测试,这很好,但是当各种测试会影响后续测试的结果时,可能会导致测试套件出现问题。)
  • @Crast,这个想法是改变全局行为,因为对 urlopen 的调用可能在某个模块的深处。将您不感兴趣的请求传递给 HTTPHandler 很简单。在大多数情况下,我会为整个测试套件使用相同的模拟开瓶器,然后重新安装原始套件。
  • @Crast,有一种方法可以卸载以前安装的开启程序。但是,它没有记录。在install 模拟开场白之前,您想用oldOpener = urllib2._opener 保存旧版本。然后,在您的单元测试teardown() 中,您使用urllib2.install_opener(oldOpener) 将其安装回来。
【解决方案2】:

如果您可以编写一个模拟 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,它实现了一些有趣的东西,例如不区分大小写的标头名称。但是,您也许可以使用响应数据简单地构造一个 HTTPMessage。

【讨论】:

  • 我非常感谢您的代码,但是 Randolpho 建议的用于与服务器通信的测试双重类将更适合这种情况。不过你帮了大忙!谢谢!
  • 无意冒犯,我同意这也是一个更好的解决方案:} 请参阅我对 Randolpho 帖子的评论。
【解决方案3】:

构建一个单独的类或模块,负责与您的外部提要进行通信。

使这个类能够成为test double。你正在使用 python,所以你在那里很漂亮;如果您使用的是 C#,我建议使用接口或虚拟方法。

在您的单元测试中,插入外部提要类的测试替身。测试您的代码是否正确使用该类,假设该类正确地与您的外部资源进行通信。让您的测试双重返回假数据而不是真实数据;测试数据的各种组合,当然还有 urllib2 可能抛出的异常。

然后……就是这样。

您无法有效地自动化依赖外部资源的单元测试,因此最好不这样做。偶尔在您的通信模块上运行集成测试,但不要将这些测试作为自动化测试的一部分。

编辑:

请注意我的答案和@Crast 的答案之间的区别。两者本质上都是正确的,但它们涉及不同的方法。在 Crast 的方法中,您在库本身上使用了一个测试替身。在我的方法中,您将库的使用抽象为一个单独的模块并测试该模块的双倍。

您使用哪种方法完全是主观的;那里没有“正确”的答案。我更喜欢我的方法,因为它允许我构建更模块化、更灵活的代码,这是我看重的。但它需要编写额外的代码,这在许多敏捷情况下可能不被重视。

【讨论】:

  • 回复。您的编辑:对于它的价值,如果我从头开始编写代码,我实际上也会采用您的方法(某种 url-getter 类)。我更喜欢编写最小的接口,只在返回时保证属性的子集,因此它们更容易进行测试双打。此外,它使依赖注入更加明确。
  • 想再投票几次。这种方法将使测试吨的模拟比模拟标准库中的东西更容易!
【解决方案4】:

您可以使用pymox 来模拟 urllib2(或任何其他)包中任何事物的行为。现在是 2010 年,你不应该编写自己的模拟类。

【讨论】:

  • +1 表示“现在是 2010...”,虽然 pymox 看起来也很有趣。
  • 2017年有unittest.mock,包含在Python3发行版中。
【解决方案5】:

我认为最简单的做法是在单元测试中实际创建一个简单的 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 的URL。

【讨论】:

  • 我考虑过,但它似乎......充其量是不愉快的。如果您认为您可以提供比上述建议更简单的代码来为通信类使用测试替身,请这样做。我很想看到这样一个快速而肮脏的服务器,只是为了我自己的教育。
  • 如果它依赖于一个网站,它就不会是一个单元测试。使用 Python 中简单的模拟对象。
  • 好的,我已经添加了一些简单的代码。我怀疑你能做比这更简单的事情。
【解决方案6】:

为什么不只是 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"

【讨论】:

    【解决方案7】:

    为了改进@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)
    

    【讨论】:

      猜你喜欢
      • 2014-05-29
      • 1970-01-01
      • 2017-02-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多