代码之家  ›  专栏  ›  技术社区  ›  Gabriel Hurley

如何对依赖于URLLIB2的模块进行单元测试?

  •  24
  • Gabriel Hurley  · 技术社区  · 15 年前

    我有一段代码,我不知道如何进行单元测试!该模块使用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...
    

    我的第一个想法是对响应进行pickle处理并加载以进行测试,但显然Urllib的响应对象是不可分割的(它引发了一个异常)。

    仅仅从响应主体保存XML并不理想,因为我的代码也使用了头信息。它被设计为作用于响应对象。

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

    那么,如何为这个编写单元测试呢?

    7 回复  |  直到 8 年前
        1
  •  25
  •   John La Rooy    15 年前

    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
    
        2
  •  9
  •   Crast    15 年前

    最好是编写一个模拟的urlopen(也可能是请求),它提供了所需的最小接口,可以像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>')
    

    当然,其中一些是很难模仿的,因为例如,我相信正常的“headers”是一个httpmessage,它实现了一些有趣的东西,比如不区分大小写的头名称。但是,您可以简单地用响应数据构造一个httpmessage。

        3
  •  6
  •   Randolpho    15 年前

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

    使该类成为 test double . 您使用的是python,所以您在这里非常出色;如果您使用的是c,我建议您使用接口或虚拟方法。

    在单元测试中,插入外部feed类的测试double。测试您的代码是否正确地使用了该类,假设该类完成了与外部资源正确通信的工作。让您的测试双重返回假数据,而不是实况数据;测试数据的各种组合,当然,URLLIB2可能会抛出可能的异常。

    嗯…就是这样。

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

    编辑:

    只是我的答案和@crast的答案之间的区别。两者基本上都是正确的,但涉及不同的方法。在Crast的方法中,您在库本身上使用了一个双测试。在我的方法中,您将库的使用抽象为一个单独的模块,并对该模块进行双重测试。

    你所使用的方法完全是主观的;那里没有“正确”的答案。我更喜欢我的方法,因为它允许我构建更模块化、更灵活的代码,这是我的价值所在。但是,从编写额外代码的角度来看,这是一个代价,在许多敏捷的情况下,这可能是不值得重视的。

        4
  •  5
  •   anthony    15 年前

    你可以用 pymox 模拟URLLIB2(或任何其他)包中任何东西的行为。现在是2010年,你不应该写你自己的模拟课。

        5
  •  1
  •   Gabe Timothy Khouri    15 年前

    我认为最简单的事情就是在单元测试中创建一个简单的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 .

        6
  •  0
  •   Tom Willis    15 年前

    为什么不只是 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
  •  0
  •   Romuald Brunet    8 年前

    为了在@john la rooy answer上提高一点,我做了一个小班,允许对单元测试进行简单的模拟。

    应该使用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)