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