Python实用库使用与浅析系列一:httmock
介绍
这个系列的第一篇文章,介绍一下httmook库使用和原理,代码只有200多行,实现的很巧妙。
应用场景:有时会需要调用外部接口,拿到返回数据用以满足当前的测试任务的需求。但是当外部接口不可用,或者没有提供测试用环境时,就需要mock接口。
pypi链接:https://pypi.org/project/httmock/
安装:pip install httmock
使用
httpmook提供两个装饰器接口:
- urlmatch
- all_requests
urlmatch拦截匹配url的请求
from httmock import urlmatch, HTTMock import requests @urlmatch(netloc=r'(.*\.)?google\.com$') def google_mock(url, request): return 'Feeling lucky, punk?' with HTTMock(google_mock): r = requests.get('http://google.com/') print r.content # 'Feeling lucky, punk?'
all_requests拦截所有请求:
from httmock import all_requests, HTTMock import requests @all_requests def response_content(url, request): return {'status_code': 200, 'content': 'Oh hai'} with HTTMock(response_content): r = requests.get('https://foo_bar') print r.status_code print r.content
如何构造返回数据:
from httmock import all_requests, response, HTTMock import requests @all_requests def response_content(url, request): headers = {'content-type': 'application/json', 'Set-Cookie': 'foo=bar;'} content = {'message': 'API rate limit exceeded'} return response(403, content, headers, None, 5, request) with HTTMock(response_content): r = requests.get('https://api.github.com/users/whatever') print r.json().get('message') print r.cookies['foo']
原理
我以第一个示例来说明:
通过with语实例一个上下文解析器的类的实例,并传入带有装饰器(urlmatch)的返回数据构造函数google_mock,这是我们通过下面几行代码能看到的:
from httmock import urlmatch, HTTMock import requests @urlmatch(netloc=r'(.*\.)?google\.com$') def google_mock(url, request): return 'Feeling lucky, punk?' with HTTMock(google_mock): r = requests.get('http://google.com/') print r.content # 'Feeling lucky, punk?'
HTTMook的初始化函数,初始化被装饰的函数google_mock
def __init__(self, *handlers): self.handlers = handlers
上下文解析器的__enter__做了什么:
def __enter__(self): self._real_session_send = requests.Session.send self._real_session_prepare_request = requests.Session.prepare_request for handler in self.handlers: handler_clean_call(handler) #制造假的send函数 def _fake_send(session, request, **kwargs): response = self.intercept(request, **kwargs) if isinstance(response, requests.Response): # this is pasted from requests to handle redirects properly: kwargs.setdefault('stream', session.stream) kwargs.setdefault('verify', session.verify) kwargs.setdefault('cert', session.cert) kwargs.setdefault('proxies', session.proxies) allow_redirects = kwargs.pop('allow_redirects', True) stream = kwargs.get('stream') timeout = kwargs.get('timeout') verify = kwargs.get('verify') cert = kwargs.get('cert') proxies = kwargs.get('proxies') gen = session.resolve_redirects( response, request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies) history = [resp for resp in gen] if allow_redirects else [] if history: history.insert(0, response) response = history.pop() response.history = tuple(history) session.cookies.update(response.cookies) return response return self._real_session_send(session, request, **kwargs) def _fake_prepare_request(session, request): """ Fake this method so the `PreparedRequest` objects contains an attribute `original` of the original request. """ prep = self._real_session_prepare_request(session, request) prep.original = request return prep #替换requests的send与prepare_request函数 requests.Session.send = _fake_send requests.Session.prepare_request = _fake_prepare_request return self
1、首先保存了requests.Session.send与requests.Session.prepare_request
2、handler_clean_call(handler),对handlers做了预处理
3、制造替换函数,_fake_send与_fake_prepare_request,并替换requests中原始的函数,作为一门动态语言的优势现在体现出来了:
#替换requests的send与prepare_request函数 requests.Session.send = _fake_send requests.Session.prepare_request = _fake_prepare_request
_fake_send函数最重要的代码:
response = self.intercept(request, **kwargs)
intercept函数的作用:执行handel函数,拿到构造的返回数据:
def intercept(self, request, **kwargs): url = urlparse.urlsplit(request.url) res = first_of(self.handlers, url, request)if isinstance(res, requests.Response):return res elif isinstance(res, dict):return response(res.get('status_code'), res.get('content'), res.get('headers'), res.get('reason'), res.get('elapsed', 0), request, stream=kwargs.get('stream', False), http_vsn=res.get('http_vsn', 11)) elif isinstance(res, (text_type, binary_type)):return response(content=res, stream=kwargs.get('stream', False)) elif res is None:return None else:raise TypeError( "Dont know how to handle response of type {0}".format(type(res)))
最后执行handle的函数:
也可以看到google_mock(url, request)的两个参数是如何传入的,这是由于_fake_send替换掉requests运行时态的send包,在执行过程中_fake_send拿到request这个实例封装参数。
def first_of(handlers, *args, **kwargs): for handler in handlers: res = handler(*args, **kwargs) if res is not None: return res
上下文解析器的__exit__做了什么:
替换掉运行时的send与prepare函数:
def __exit__(self, exc_type, exc_val, exc_tb): #恢复 requests.Session.send = self._real_session_send requests.Session.prepare_request = self._real_session_prepare_request