前言
进行接口自动化测试时,为了方便调试,通常我们会增加一些日志来打印请求 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_code
和 response.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]:
太棒了,信息一目了然。
最后
这样我们就使用装饰器优雅的解决了问题,这样以后想要变更打印内容,只需要更改装饰器即可,测试用例都不受影响,提高了代码的复用性、扩展性和可维护性,同时保持代码的透明性和灵活性,使得代码更加清晰、简洁和易于理解。