¿Cómo unidad de prueba de un módulo que se basa en urllib2?
-
21-09-2019 - |
Pregunta
Tengo un trozo de código que no puedo encontrar la manera de probar la unidad! El módulo tira de contenidos a partir de cargas externas XML (Twitter, Flickr, YouTube, etc.) con urllib2. He aquí algunos pseudo-có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 de estibar la respuesta y la carga de la prueba, pero al parecer objeto de respuesta de urllib es unserializable (se genera una excepción).
Sólo guardar el XML desde el cuerpo de la respuesta no es ideal, porque mi código utiliza la información de la cabecera también. Está diseñado para actuar sobre un objeto respuesta.
Y, por supuesto, contando con una fuente externa de datos en una unidad de prueba es un horribles idea.
Entonces, ¿cómo puedo escribir una prueba unitaria para esto?
Solución
urllib2 tiene unas funciones llamadas build_opener()
y install_opener()
que se debe utilizar para burlarse del 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
Otros consejos
Sería mejor si usted podría escribir un simulacro urlopen (y posiblemente Solicitud), que proporciona la interfaz mínima necesaria para comportarse como la versión de urllib2. A continuación, tendría que tener su función / método que utiliza es capaz de aceptar este urlopen simulacro de alguna manera, y utilizar urllib2.urlopen
lo contrario.
Esta es una buena cantidad de trabajo, pero vale la pena. Recuerde que el pitón es muy amigable para Duck Typing, por lo que sólo necesita proporcionar cierta apariencia de las propiedades del objeto respuesta a burlarse 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'<xml document>')
Por supuesto, algunos de estos son difíciles de burlarse, porque por ejemplo yo creo que las "cabeceras" normales es un HttpMessage que implementa cosas divertidas, como nombres de encabezado de mayúsculas y minúsculas. Sin embargo, es posible que pueda simplemente construir un HttpMessage con sus datos de respuesta.
Construir una clase separada o módulo responsable de comunicarse con sus alimentaciones externas.
Hacer esta clase capaz de ser un prueba doble . Usted está utilizando Python, por lo que está bastante de oro allí; si estuviera usando C #, me gustaría sugerir ya sea en la interfaz o virtuales métodos.
En la prueba de unidad, inserte una prueba doble de la clase de alimentación externa. Prueba de que el código utiliza la clase correcta, en el supuesto de que la clase hace el trabajo de comunicarse correctamente con sus recursos externos. Tener su prueba de doble retorno de datos falsos en lugar de datos en tiempo real; probar varias combinaciones de los datos y, por supuesto, las posibles excepciones urllib2 podría lanzar.
A y ... eso es todo.
No se puede automatizar de manera efectiva las pruebas unitarias que dependen de fuentes externas, por lo que es el mejor de no hacerlo . Ejecutar una prueba de integración de vez en cuando en su módulo de comunicación, pero no incluyen las pruebas como parte de sus pruebas automatizadas.
Editar
Sólo una nota en la diferencia entre mi respuesta y la respuesta de @ Crast. Ambos son esencialmente correcta, pero implican diferentes enfoques. En el enfoque de Crast, se utiliza una doble prueba en la propia biblioteca. En mi enfoque, se resumen el uso de la biblioteca de distancia en un módulo separado y doble prueba de ese módulo.
¿Qué enfoque que se utiliza es totalmente subjetivo; no hay una respuesta "correcta" allí. Prefiero que mi enfoque, ya que me permite construir más modular, flexible, código, algo que valoro. Pero tiene un costo en términos de código adicional para escribir, algo que no puede ser valorado en muchas situaciones ágiles.
Puede utilizar pymox burlarse del comportamiento de cualquier cosa y todo en el urllib2 (o cualquier otra) paquete. Es 2010, no se debe escribir sus propias clases simuladas.
Creo que la cosa más fácil de hacer es crear realmente un servidor web sencillo en su unidad de prueba. Al iniciar la prueba, crear un nuevo hilo que escucha en un puerto arbitrario y cuando un cliente se conecta sólo devuelve un conjunto conocido de encabezados y XML, y luego termina.
Puedo elaborar si necesita más información.
Aquí hay algo de 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 la prueba de la unidad, llame serve_data()
luego llame a su código que solicita una dirección URL que se parece a http://localhost:12345/anythingyouwant
.
¿Por qué no burlarse de un sitio web que devuelve la respuesta se puede esperar? a continuación, iniciar el servidor en un hilo en la configuración y matarlo en el desmontaje. Terminé haciendo esto para probar código que se envía un correo electrónico al burlarse de un servidor SMTP y funciona muy bien. Sin duda algo más trivial se podía hacer por 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"
Tratar de mejorar un poco en respuesta @ John-la-Rooy, he hecho una pequeña clase de burla simple para permitir pruebas de unidad
En caso de trabajar con el pitón 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)
Se utiliza como esto:
class TestOther(unittest.TestCase):
def setUp(self):
previous = MockHTTPHandler.install()
self.addCleanup(MockHTTPHandler.remove, previous)