كيف يمكنني اختبار الوحدة وحدة تعتمد على urllib2؟
-
21-09-2019 - |
سؤال
لدي قطعة رمز لا يمكنني معرفة كيفية اختبار الوحدة! تقوم الوحدة بسحب المحتوى من خلاصات 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 أمر غير محترم (يثير استثناء).
مجرد حفظ XML من هيئة الاستجابة ليس مثاليًا ، لأن الكود الخاص بي يستخدم معلومات الرأس أيضًا. إنه مصمم للعمل على كائن استجابة.
وبالطبع ، فإن الاعتماد على مصدر خارجي للبيانات في اختبار الوحدة هو رهيب فكرة.
فكيف أكتب اختبار الوحدة لهذا؟
المحلول
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
نصائح أخرى
سيكون من الأفضل لو تمكنت من كتابة عنوان URLOPEN وهمية (وربما طلب) والذي يوفر الحد الأدنى للواجهة المطلوبة للتصرف مثل إصدار 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 مع بيانات الاستجابة الخاصة بك.
قم بإنشاء فئة أو وحدة نمطية منفصلة مسؤولة عن التواصل مع الخلاصات الخارجية.
اجعل هذا الفصل قادرًا على أن يكون اختبار مزدوج. أنت تستخدم بيثون ، لذلك أنت ذهبي جدا هناك ؛ إذا كنت تستخدم C#، أقترح إما في الواجهة أو الأساليب الافتراضية.
في اختبار الوحدة ، أدخل اختبارًا مزدوجًا لفئة التغذية الخارجية. اختبر أن الكود الخاص بك يستخدم الفصل بشكل صحيح ، على افتراض أن الفصل يقوم بعمل التواصل مع مواردك الخارجية بشكل صحيح. اجعل الاختبار المزدوج بيانات مزيفة بدلاً من البيانات الحية ؛ اختبار مجموعات مختلفة من البيانات وبالطبع الاستثناءات المحتملة التي يمكن أن يرميها urllib2.
Aand ... هذا كل شيء.
لا يمكنك أتمتة اختبارات الوحدة التي تعتمد بشكل فعال على المصادر الخارجية ، لذلك من الأفضل أن تكون أفضل لا تفعل ذلك. قم بإجراء اختبار تكامل عرضي على وحدة الاتصال الخاصة بك ، ولكن لا تتضمن تلك الاختبارات كجزء من الاختبارات الآلية.
يحرر:
مجرد ملاحظة حول الفرق بين إجابتي وإجابة @Crast. كلاهما صحيح بشكل أساسي ، لكنه ينطوي على طرق مختلفة. في نهج Crast ، يمكنك استخدام اختبار مزدوج على المكتبة نفسها. في مقاربي ، تقوم بتجريد استخدام المكتبة بعيدًا في وحدة نمطية منفصلة واختبار هذه الوحدة النمطية.
ما هو النهج الذي تستخدمه هو شخصي تمامًا ؛ لا يوجد إجابة "صحيحة" هناك. أنا أفضل نهجتي لأنه يسمح لي ببناء رمز أكثر وحدات ومرنة ، شيء أقدره. لكنه يأتي بتكلفة من حيث التعليمات البرمجية الإضافية للكتابة ، وهو أمر قد لا يتم تقديره في العديد من المواقف الرشيقة.
يمكنك استخدام pymox للسخرية من سلوك أي شيء وكل شيء في حزمة urllib2 (أو أي أخرى). إنه عام 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
.
لماذا لا فقط سخرية موقع ويب هذا يعيد الاستجابة التي تتوقعها؟ ثم ابدأ الخادم في مؤشر ترابط في الإعداد وقتله في teardown. انتهى بي الأمر إلى القيام بذلك للاختبار رمز من شأنه أن يرسل بريدًا إلكترونيًا عن طريق السخرية من خادم 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"
في محاولة للتحسين قليلاً على إجابة @John-La-Rrooy ، قمت بعمل فصل صغير يسمح بسخرية بسيطة لاختبارات الوحدة
يجب أن تعمل مع بيثون 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)