Как выполнить модульное тестирование модуля, основанного на urllib2? - PullRequest
25 голосов
/ 17 февраля 2010

У меня есть кусок кода, который я не могу понять, как выполнить модульное тестирование! Модуль извлекает контент из внешних XML-каналов (твиттер, 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 не может быть проанализирован (возникает исключение).

Простое сохранение XML из тела ответа не идеально, потому что мой код тоже использует информацию заголовка. Он предназначен для воздействия на объект ответа.

И, конечно, полагаться на внешний источник данных в модульном тесте - это ужасная идея.

Так как мне написать для этого модульный тест?

Ответы [ 7 ]

25 голосов
/ 17 февраля 2010

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
9 голосов
/ 17 февраля 2010

Было бы лучше, если бы вы могли написать фиктивный 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 с вашими данными ответа.

6 голосов
/ 17 февраля 2010

Создайте отдельный класс или модуль, отвечающий за связь с вашими внешними каналами.

Сделать этот класс способным быть двойным тестом . Вы используете Python, так что вы там довольно золотые; если бы вы использовали C #, я бы предложил использовать интерфейсные или виртуальные методы.

В вашем модульном тесте вставьте двойной тест внешнего класса подачи. Проверьте, правильно ли ваш код использует класс, предполагая, что класс выполняет работу с вашими внешними ресурсами правильно. Сделайте так, чтобы ваш тест дважды возвращал фальшивые данные, а не живые данные; протестировать различные комбинации данных и, конечно, возможные исключения urllib2.

А ... вот и все.

Вы не можете эффективно автоматизировать модульные тесты, основанные на внешних источниках, поэтому лучше всего не делать этого . Запустите случайный интеграционный тест на вашем коммуникационном модуле, но не включайте эти тесты в свои автоматические тесты.

Редактировать:

Просто заметка о разнице между моим ответом и ответом @ Crast. И то, и другое по сути правильно, но они предполагают разные подходы. В подходе Crast вы используете тестовый дубль на самой библиотеке. В моем подходе вы абстрагируете использование библиотеки в отдельный модуль и тестируете двойной модуль.

Какой подход вы используете, является полностью субъективным; там нет "правильного" ответа. Я предпочитаю свой подход, потому что он позволяет мне создавать более модульный, гибкий код, что я ценю. Но это обходится дорого с точки зрения написания дополнительного кода, что не может быть оценено во многих гибких ситуациях.

5 голосов
/ 17 февраля 2010

Вы можете использовать pymox , чтобы высмеивать поведение всего и вся в пакете urllib2 (или любом другом).Это 2010 год, вы не должны писать свои собственные фиктивные классы.

1 голос
/ 17 февраля 2010

Я думаю, что самое простое - создать простой веб-сервер в вашем модульном тесте. Когда вы запускаете тест, создайте новый поток, который прослушивает какой-то произвольный порт, и когда клиент подключается, просто возвращает известный набор заголовков и 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(), а затем вызовите код, запрашивающий URL-адрес, похожий на http://localhost:12345/anythingyouwant.

0 голосов
/ 31 декабря 2016

Пытаясь немного улучшить ответ @ john-la-rooy, я организовал небольшой класс, позволяющий просто издеваться над юнит-тестами

Должен работать с питоном 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)
0 голосов
/ 17 февраля 2010

Почему бы просто не высмеять веб-сайт , который возвращает ожидаемый ответ? затем запустить сервер в потоке в настройке и убить его в демонтаже. Я закончил тем, что сделал это для тестирования кода, который отправлял бы электронную почту, издеваясь над 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"
...