¿Cómo puedo probar un módulo que se basa en urllib2?

¡Tengo un código que no puedo averiguar cómo realizar una prueba de unidad! El módulo extrae contenido de fonts XML externas (twitter, flickr, youtube, etc.) con urllib2. Aquí hay un pseudocódigo para ello:

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... 

Mi primer pensamiento fue recoger la respuesta y cargarla para probarla, pero aparentemente el objeto de respuesta de urllib no se puede serializar (genera una excepción).

No es ideal simplemente guardar el XML del cuerpo de la respuesta, porque mi código también usa la información del encabezado. Está diseñado para actuar sobre un objeto de respuesta.

Y, por supuesto, confiar en una fuente externa de datos en una prueba unitaria es una idea horrible .

Entonces, ¿cómo escribo una prueba de unidad para esto?

urllib2 tiene una función llamada build_opener() y install_opener() que deberías usar para urlopen() el comportamiento de 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 

Sería mejor si pudiera escribir un urlopen simulado (y posiblemente una solicitud) que proporcione la interfaz mínima requerida para comportarse como la versión de urllib2. Entonces necesitarías tener tu función / método que la use capaz de aceptar este simulacro de urlopen de alguna manera, y usar urllib2.urlopen contrario.

Esta es una buena cantidad de trabajo, pero vale la pena. Recuerda que python es muy amigable con el patito, por lo que solo necesitas proporcionar una apariencia de las propiedades del objeto de respuesta para burlarte de él.

Por ejemplo:

 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'') 

Por supuesto, algunos de estos son difíciles de burlar, porque, por ejemplo, creo que los “encabezados” normales son un HTTPMessage que implementa cosas divertidas como nombres de encabezados que no distinguen entre mayúsculas y minúsculas. Pero es posible que simplemente pueda construir un HTTPMessage con sus datos de respuesta.

Cree una clase o módulo independiente responsable de comunicarse con sus fonts externas.

Haz que esta clase sea capaz de ser una prueba doble . Estás usando python, así que estás bastante dorado allí; Si estuviera usando C #, sugeriría ya sea en la interfaz o en métodos virtuales.

En su prueba de unidad, inserte una prueba doble de la clase de alimentación externa. Pruebe que su código usa la clase correctamente, asumiendo que la clase hace el trabajo de comunicarse con sus recursos externos correctamente. Haga que su prueba doble devuelva datos falsos en lugar de datos en vivo; Pruebe varias combinaciones de los datos y, por supuesto, las posibles excepciones que urllib2 podría lanzar.

Aand … eso es todo.

No puede automatizar de manera efectiva las pruebas unitarias que dependen de fonts externas, por lo que es mejor no hacerlo . Ejecute una prueba de integración ocasional en su módulo de comunicación, pero no incluya esas pruebas como parte de sus pruebas automatizadas.

Editar:

Solo una nota sobre la diferencia entre mi respuesta y la respuesta de @Crast. Ambos son esencialmente correctos, pero implican diferentes enfoques. En el enfoque de Crast, utiliza una prueba doble en la propia biblioteca. En mi enfoque, abstraes el uso de la biblioteca en un módulo separado y pruebas el doble de ese módulo.

El enfoque que utilices es completamente subjetivo; no hay una respuesta “correcta” allí. Prefiero mi enfoque porque me permite construir código más modular y flexible, algo que valoro. Pero tiene un costo en términos de código adicional para escribir, algo que puede no ser valorado en muchas situaciones ágiles.

Puede usar pymox para simular el comportamiento de cualquier cosa y todo en el paquete urllib2 (o cualquier otro). Es 2010, no deberías estar escribiendo tus propias clases de simulacros.

Creo que lo más fácil es crear un servidor web simple en su prueba de unidad. Cuando inicie la prueba, cree un nuevo hilo que escuche en algún puerto arbitrario y cuando un cliente se conecte, solo devolverá un conjunto conocido de encabezados y XML, y luego terminará.

Puedo explicarte si necesitas más información.

Aquí hay un código:

 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()) 

Para ejecutar su prueba de unidad, llame a serve_data() luego llame a su código que solicita una URL que se parece a http://localhost:12345/anythingyouwant .

¿Por qué no burlarse de un sitio web que devuelve la respuesta que espera? a continuación, inicie el servidor en un hilo en la configuración y elimínelo en el desassembly. Terminé haciendo esto para probar el código que enviaría un correo electrónico al simular un servidor smtp y funciona muy bien. Seguramente algo más trivial podría hacerse para 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" 

Tratando de mejorar un poco en la respuesta de @ john-la-rooy, he hecho una pequeña clase que permite burlas simples para pruebas de unidad

Debería trabajar con python 2 y 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) 

Utilizado de esta manera:

 class TestOther(unittest.TestCase): def setUp(self): previous = MockHTTPHandler.install() self.addCleanup(MockHTTPHandler.remove, previous)