Как я 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)