Как я unit test модуль, который полагается на urllib2?
У меня есть код, который я не могу понять, как unit test! Модуль извлекает содержимое из внешних 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 является unserializable (он вызывает исключение).
Простое сохранение XML из тела ответа не является идеальным, потому что мой код также использует информацию заголовка. Он предназначен для работы с объектом-ответчиком.
И, конечно, полагаться на внешний источник данных в unit test - это ужасная идея.
Итак, как мне написать unit test для этого?
Ответы
Ответ 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
Ответ 2
Было бы лучше, если бы вы могли написать mock 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 с вашими данными ответа.
Ответ 3
Создайте отдельный класс или модуль, отвечающий за связь с вашими внешними фидами.
Сделайте этот класс способным test double. Вы используете python, так что вы там довольно золотистый; если вы используете С#, я бы предложил либо интерфейс, либо виртуальные методы.
В вашем unit test вставьте тестовый двойник внешнего класса подачи. Проверьте, правильно ли используется ваш код, считая, что класс правильно работает с вашими внешними ресурсами. Имейте тестовые двойные обратные поддельные данные, а не живые данные; проверить различные комбинации данных и, конечно же, возможные исключения, которые мог бы выполнить urllib2.
A и... что он.
Вы не можете эффективно автоматизировать модульные тесты, которые полагаются на внешние источники, поэтому вам лучше не делать этого. Выполняйте случайный интеграционный тест на вашем коммуникационном модуле, но не включайте эти тесты как часть ваших автоматических тестов.
Изменить:
Просто обратите внимание на разницу между ответом и ответом @Crast. Оба они по существу правильны, но они связаны с различными подходами. В подходе Crast вы используете двойной тест в самой библиотеке. В моем подходе вы отвлеките использование библиотеки в отдельный модуль и дважды проверите этот модуль.
Какой подход вы используете, является полностью субъективным; там нет "правильного" ответа. Я предпочитаю свой подход, потому что он позволяет мне строить более модульный, гибкий код, что-то, что я ценю. Но это связано с ценой с точки зрения дополнительного кода для написания, что не может быть оценено во многих гибких ситуациях.
Ответ 4
Вы можете использовать pymox, чтобы высмеять поведение всего и всего в пакете urllib2 (или любом другом). В 2010 году вы не должны писать свои собственные макеты.
Ответ 5
Я думаю, что проще всего создать простой веб-сервер в unit test. Когда вы начинаете тест, создайте новый поток, который прослушивает какой-то произвольный порт, и когда клиент подключается, просто возвращает известный набор заголовков и 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())
Чтобы запустить unit test, вызовите serve_data()
, затем вызовите свой код, который запрашивает URL-адрес, который выглядит как http://localhost:12345/anythingyouwant
.
Ответ 6
Почему бы просто издеваться над сайтом, который возвращает ответ, который вы ожидаете? затем запустите сервер в потоке в настройке и убейте его при разрыве. Я закончил делать это для тестирования кода, который будет отправлять электронную почту, насмехаясь над 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)