装饰器大显身手:优雅解决请求前后调试信息输出

2023年 9月 28日 54.9k 0

前言

进行接口自动化测试时,为了方便调试,通常我们会增加一些日志来打印请求 URL、方法、参数、响应状态码和内容。常见的笨办法,当然是直接在请求之后增加日志输出。但这有一个问题,会造成大量的冗余代码。那我们就想办法解决?带着这个实际场景,我们一起看看该如何优化呢?

笨办法

我们看看如果使用笨办法,写出来的代码是这样的:

import requests
​
def test_demo():
    response = requests.get(
        url="https://example.com/",
    )
    print(f"Request URL: {response.request.url}")
    print(f"Request Method: {response.request.method}")
    print(f"Request Body: {response.request.body}")
    print(f"Response Status Code: {response.status_code}")
    print(f"Response Content: {response.content}")
​
    assert response.status_code == 200

这是一个简单的案例,我们使用requests库进行HTTP请求后,使用 response.request 对象访问请求属性,使用 response.status_coderesponse.content 属性访问响应相关信息。最终打印出请求 URL、方法、参数、响应状态码和内容。

可以看到,如果每个测试case都这样写打印,很麻烦也不利于代码的维护阅读。假设我想新增打印headers还需要每个case上新增打印语句,可见重复工作量是很大的。

该如何解决优化呢?笔者这里使用装饰器来解决。

使用装饰器解决

介绍实现代码之前,有同学可能不明白装饰器该如何使用?想进一步了解的同学,可以参考笔者之前的文章:基于FastApi框架测试平台(14)-装饰器复习

首先,我们简单封装一下requests请求,比如这样:

class HttpRequest:
​
    @request
    def get(self, url, params=None, **kwargs):
        return requests.get(url, params=params, **kwargs)

我们看到,这里使用的装饰器@request,该装饰器将实现笨办法中的那些打印。

request装饰器实现,先附上实现代码,在做一个具体讲解

def request(func):
    def wrapper(*args, **kwargs):
        func_name = func.__name__
        try:
            url = list(args)[1]
        except IndexError:
            url = kwargs.get("url", "")
        if (Panda.base_url is not None) and (url.startswith("http") is False):
            url = Panda.base_url + url
​
        img_file = False
        file_type = url.split(".")[-1]
        if file_type in IMG:
            img_file = True
​
        log.info(f"[method]: {func_name.upper()}      [url]: {url} ")
        auth = kwargs.get("auth", None)
        headers = kwargs.get("headers", None)
        cookies = kwargs.get("cookies", None)
        params = kwargs.get("params", None)
        data = kwargs.get("data", None)
        json_ = kwargs.get("json", None)
        files = kwargs.get("files", None)
        if auth is not None:
            log.debug(f"[auth]:\n{auth}")
        if headers is not None:
            log.debug(f"[headers]:\n{formatting(headers)}")
        if cookies is not None:
            log.debug(f"[cookies]:\n{formatting(cookies)}")
        if params is not None:
            log.debug(f"[params]:\n{formatting(params)}")
        if data is not None:
            log.debug(f"[data]:\n{formatting(data)}")
        if json_ is not None:
            log.debug(f"[json]:\n{formatting(json_)}")
        if files is not None:
            log.debug(f"[files]:\n{files}")
​
        r = func(*args, **kwargs)
​
        ResponseResult.request = r.request
        ResponseResult.status_code = r.status_code
        if ResponseResult.status_code == 200 or ResponseResult.status_code == 304:
            log.info(f"successful with status {ResponseResult.status_code}")
        else:
            log.warning(f"unsuccessful with status {ResponseResult.status_code}")
        resp_time = r.elapsed.total_seconds()
        try:
            resp = r.json()
            log.debug(f"[type]: json      [time]: {resp_time}")
            log.debug(f"[response]:\n {formatting(resp)}")
            ResponseResult.response = resp
        except BaseException as msg:
            log.debug("[warning]: failed to convert res to json, try to convert to text")
            log.trace(f"[warning]: {msg}")
            if img_file is True:
                log.debug(f"[type]: {file_type}      [time]: {resp_time}")
                ResponseResult.response = r.content
            else:
                r.encoding = 'utf-8'
                log.debug(f"[type]: text      [time]: {resp_time}")
                log.debug(f"[response]:\n {r.text}")
                ResponseResult.response = r.text
        return r
    return wrapper

建议不懂的小伙伴先去看一看装饰器的使用。下面我们做一个简单解释:

  • func_name = func.__name__:获取被装饰函数的名称。
  • 获取请求的 URL:
try:
    url = list(args)[1]
except IndexError:
    url = kwargs.get("url", "")

这里通过检查参数列表来获取 URL,如果没有传递 URL 参数,则从关键字参数中获取。

  • 检查是否配置了基本URL,并根据需要拼接完整的URL:
if (Panda.base_url is not None) and (url.startswith("http") is False):
    url = Panda.base_url + url
  • 判断请求的文件类型:
file_type = url.split(".")[-1]
if file_type in IMG:
    img_file = True

这里将 URL 中的文件类型与预定义的图片文件类型列表 IMG 进行对比,如果匹配则将 img_file 设置为 True。

  • 输出请求的方法和 URL:
log.info(f"[method]: {func_name.upper()}      [url]: {url} ")
  • 获取请求的认证、头部信息、Cookie、参数、数据、JSON 和文件等内容:
auth = kwargs.get("auth", None)
headers = kwargs.get("headers", None)
cookies = kwargs.get("cookies", None)
params = kwargs.get("params", None)
data = kwargs.get("data", None)
json_ = kwargs.get("json", None)
files = kwargs.get("files", None)

这里使用 kwargs.get() 方法获取关键字参数的值。

  • 如果存在认证信息、头部信息、Cookie、参数、数据、JSON 或文件,输出对应的调试信息:
if auth is not None:
    log.debug(f"[auth]:\n{auth}")
if headers is not None:
    log.debug(f"[headers]:\n{formatting(headers)}")
if cookies is not None:
    log.debug(f"[cookies]:\n{formatting(cookies)}")
if params is not None:
    log.debug(f"[params]:\n{formatting(params)}")
if data is not None:
    log.debug(f"[data]:\n{formatting(data)}")
if json_ is not None:
    log.debug(f"[json]:\n{formatting(json_)}")
if files is not None:
    log.debug(f"[files]:\n{files}")

这里使用了一个辅助函数 formatting() 来格式化输出。

  • 调用被装饰函数,并将返回结果保存在变量 r 中:
r = func(*args, **kwargs)
  • 设置响应结果的请求和状态码:
ResponseResult.request = r.request
ResponseResult.status_code = r.status_code
  • 根据状态码输出成功或警告信息:
if ResponseResult.status_code == 200 or ResponseResult.status_code == 304:
    log.info(f"successful with status {ResponseResult.status_code}")
else:
    log.warning(f"unsuccessful with status {ResponseResult.status_code}")
  • 获取响应时间,并尝试将响应内容解析为 JSON 格式。如果解析失败,尝试将响应内容转换为文本格式或保持二进制格式(如果是图片文件):
resp_time = r.elapsed.total_seconds()
try:
    resp = r.json()
    log.debug(f"[type]: json      [time]: {resp_time}")
    log.debug(f"[response]:\n {formatting(resp)}")
    ResponseResult.response = resp
except BaseException as msg:
    log.debug("[warning]: failed to convert res to json, try to convert to text")
    log.trace(f"[warning]: {msg}")
    if img_file is True:
        log.debug(f"[type]: {file_type}      [time]: {resp_time}")
        ResponseResult.response = r.content
    else:
        r.encoding = 'utf-8'
        log.debug(f"[type]: text      [time]: {resp_time}")
        log.debug(f"[response]:\n {r.text}")
        ResponseResult.response = r.text

这里使用了一个全局变量 ResponseResult 来保存响应结果。

  • 返回最初被装饰的函数的执行结果 r

总结一下,整个装饰器函数的作用是在请求发出之前和响应返回之后,打印一些调试信息,并将响应结果记录到全局变量中。这样可以方便地对请求和响应进行日志记录和调试。

那我们看看成果:

http_request = HttpRequest()
​
def test_demo():
    http_request.get(
        url="https://example.com/",
    )

执行之后,会发现已经打印出了想要的信息

2023-09-28 16:25:33 | INFO     | request.py | [method]: GET      [url]: https://example.com/
2023-09-28 16:25:36 | INFO     | request.py | successful with status 200
2023-09-28 16:25:36 | DEBUG    | request.py | [type]: json      [time]: 3.249797
2023-09-28 16:25:36 | DEBUG    | request.py | [response]:

太棒了,信息一目了然。

最后

这样我们就使用装饰器优雅的解决了问题,这样以后想要变更打印内容,只需要更改装饰器即可,测试用例都不受影响,提高了代码的复用性、扩展性和可维护性,同时保持代码的透明性和灵活性,使得代码更加清晰、简洁和易于理解。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论