接口自动化 – 发送请求+响应校验

以 QQ 内一个可以随意访问的请求为例:

  //mma.qq.com/mqqactivity/cgi/monitor/report

请求结果:

  {“code”: 100100}

   

接下来就针对这个“接口请求”写一个简单的校验脚本(拼接的参数只做参考):

思路

发出一个接口请求,并对请求结果进行校验,比如请求是否成功,返回值的类型以及内容是否符合预期

原因

请求失败可能是服务器挂了,也可能是超时或者接口变更;返回值类型如果不对,可能导致客户端崩溃或者无法处理及展示;返回值内容的检查就是比较基础的了,比如和数据库或者通过计算进行判断是否准确、满足需要

 

import json
import requests

qq_url = "//mma.qq.com/mqqactivity/cgi/monitor/report?"
qq_para = {
    "num": "123",
    "test": "ceshi"
}
# 参数要使用json格式,所以用json.dumps()进行转换传输
r = requests.get(qq_url, json.dumps(qq_para))
print(r)
print(r.text)
print(r.json())
code = r.json()["code"]

if __name__ == "__main__":
    assert r.status_code == 200, "response status code is {}".format(r.status_code)
    assert isinstance(code, int), "code type is {}".format(type(code))
    assert code == 100100, "code is {}".format(code)

 

说明

1、首先需要使用 pip 命令下载 requests 库,导入 json 和 requests 模块 ;

2、设置俩变量 qq_url 和 qq_para,也可以设置成三个变量:host、path、para ;

3、使用 requests.get 方法发送请求并将响应结果赋值给变量 r ;

4、分别输出 r、r.text、r.json(),也可以把 r.status_code 打印出来;

5、将 r.json()[“code”] 的值赋给 code ,然后对响应状态码、响应结果的类型和值进行校验。

 

运行结果:

 

 

更改三个断言中的判断条件,看看断言失败后是否会给出正确的提示信息:

 

如图,当第一条断言失败后,后面的 case 就不会继续执行,而是直接抛出一个 Traceback ,告诉我们断言失败并输出我们指定的信息内容,这样可以方便我们快速定位问题,知道到底是哪里出现了问题。当然,不写也是 OK 的:

 

看运行结果,是满足预期的。下面我们就详细的了解下代码中的 requests.get 方法,源码如下:

 

看注释,就是发送一个 GET 请求,第一个参数是 url ;第二个是参数 params 默认值为None,所以是可选的,可传可不传;第三个参数 **kwargs 是可变长关键字参数,key – value 形式的,也是可选参数;返回的是个响应对象,可以通过句点的方式使用对象中的属性,比如:“.text”字符串形式的响应内容(str)、“.json()”使用 json 处理过的响应数据(dict)、“.status_code”HTTP的请求返回状态,也就是响应状态码(比如:200、404…):

 

下面就根据上面的简版代码做一版优化:首先,拆分一下,数据或者通用变量都放在一个文件中(qq_data.py),请求放在一个文件中(qq_report.py),响应数据的校验放在一个文件中(test_qq_report.py),这样不管要增加多少个接口,都比较好处理。

  

qq_data.py

 1 """
 2 基础数据
 3 """
 4 
 5 qq_host = "//mma.qq.com/"
 6 qq_path = "mqqactivity/cgi/monitor/report?"
 7 qq_para = {
 8     "num": "123",
 9     "test": "ceshi"
10 }

 

qq_report.py

 1 """
 2 随便校验一个网络请求
 3 """
 4 
 5 import requests
 6 import unittest
 7 from homework_class import qq_data
 8 
 9 
10 class Re(unittest.TestCase):
11 
12     def re_url(self, **kwargs):
13         """拼接请求连接"""
14         host = qq_data.qq_host
15         path = qq_data.qq_path
16         if kwargs:
17             para = ""
18             for key, value in kwargs.items():
19                 para += "{}={}&".format(key, value)
20             self.url = host + path + para
21             return self.url[:-1]
22         else:
23             self.url = host + path
24             return self.url
25 
26     def re_response(self, **kwargs):
27         """获取响应数据,并校验请求是否成功"""
28         obj = requests.get(self.re_url(**kwargs))
29         code = obj.status_code
30         self.assertEqual(code, 200, "code is {}, url is {}".format(code, self.re_url(**kwargs)))
31         return obj

 

test_qq_report.py

 1 """
 2 随便校验一个网络请求
 3 """
 4 import unittest
 5 from homework_class import qq_report, qq_data
 6 
 7 
 8 class QQReport(unittest.TestCase):
 9 
10     @classmethod
11     def setUpClass(cls):
12         cls.res = qq_report.Re()
13         cls.obj = cls.res.re_response(**qq_data.qq_para)
14         print(cls.obj)
15         cls.code = cls.obj.json()["code"]
16 
17     def test_code_type(self):
18         """检查code类型是否为int类型"""
19         print(type(self.code))
20         self.assertTrue(isinstance(self.code, int))
21 
22     def test_code_value(self):
23         """检查code是否为100100"""
24         print(self.code)
25         self.assertEqual(self.code, 100100)
26 
27 if __name__ == "__main__":
28     unittest.main()

 

运行一下:

 

qq_data.py 基础数据部分没啥可说的,后面可以直接添加不同的host或者相同 host 的不同 path 或者不同场景所需要的参数。

  

qq_report.py 请求处理和响应获取部分,这里面用到了基础数据模块中的数据,也用到了 unittest 模块,因为在 Re 类中继承了 unittest.TestCase 类,在 re_response 方法中使用断言校验了请求是否发送成功(因为如果请求失败了,后面的校验其实也没必要,所以就写在这里了。但是如果多个请求互不影响,其实可以放在test里面做校验,避免一条请求不通过其他的case也跑不了的情况);在 re_url 方法中将 host、path 以及可能存在的参数进行拼接,并将拼接好的url返回回来。

  

test_qq_report.py 中最先定义了一个 setUpClass 类方法,用 @classmethod 装饰器进行了装饰,所有 case 运行前只运行一次(可以了解下它和 setUp() 方法的区别);后面有两个以 test_ 开头的测试方法,分别校验了响应数据的类型和值是否符合预期。

  

相关内容介绍:

      – unittest回顾

      – setUp()方法

      – import导入

      – 变长参数

      – 装饰器

      – 继承

      – 类  

  

接下来再补充一些日志,让运行结果看起来更清楚些:

 

过程

新建一个 log.py 文件,因为我只想让它在控制台中输出时间、脚本文件名、以及我所需要输出的信息日志,所以需要先导入 logging 模块,建一个 Log 类,在构造方法中创建 logger 并设置日志等级,然后设置日志输出的格式:

 1 import inspect
 2 import logging
 3 
 4 
 5 class Log(object):
 6 
 7     def __init__(self, name=None):
 8         if name:
 9             called_name = name
10         else:
11             called_name = str(inspect.stack()[1][1]).split("/")[-1]
12         self.logger = logging.getLogger(called_name)
13         self.logger.setLevel(logging.DEBUG)
14         # 日志输出格式
15         self.fmt = logging.Formatter('[%(asctime)s] - %(name)s -> %(levelname)s: %(message)s')
16 
17     def log_print(self, level,  msg):
18         # 创建一个终端Handler,用于输出到控制台
19         console_sh = logging.StreamHandler()
20         console_sh.setLevel(logging.DEBUG)
21         # fh = logging.FileHandler('log.txt', mode='w', encoding='UTF-8')
22         # fh.setLevel(logging.DEBUG)
23         console_sh.setFormatter(self.fmt)
24 
25         self.logger.addHandler(console_sh)
26         if level == "debug":
27             self.logger.debug(msg)
28         elif level == "info":
29             self.logger.info(msg)
30         elif level == "warning":
31             self.logger.warning(msg)
32         elif level == "error":
33             self.logger.error(msg)
34         elif level == "critical":
35             self.logger.critical(msg)
36         self.logger.removeHandler(console_sh)
37 
38     def debug(self, msg):
39         self.log_print("debug", msg)
40 
41     def info(self, msg):
42         self.log_print("info", msg)
43 
44     def warning(self, msg):
45         self.log_print("warning", msg)
46 
47     def error(self, msg):
48         self.log_print("error", msg)
49 
50     def critical(self, msg):
51         self.log_print("critical", msg)

 

我们也可以看一下 Formatter 的源码中支持哪些字段的设置:

 

我这里只用到了 %(asctime)s、%(name)s、%(levelname)s、%(message)s 四个字段,对应的输出样式为:

 [2021-07-02 20:18:11,047] – test_qq_report.py -> INFO: ———- setUpClass ———-

 

下面的 log_print 方法中先创建一个终端 Handler 并且设置等级和输出格式,然后根据不同的等级调用不同的方法,这里面判断的 5 个等级就是 levelname 对应的等级。之后哪个地方需要输出日志,导入日志模块,创建完实例,调用一下对应等级的方法就可以了。

  

这里面还要详细说一下为什么它能够输出对应的脚本文件名字,很牛批的…

  

就是第 11 行代码中用到的  inspect.stack() 获取调用栈,返回的内容是个包含元组对象的列表,代码中 inspect.stack()[1][1] 返回的就是:列表中第二个元组对象,取这个对象中第二个元素的值。这一整行的代码就是将刚刚取出来的值转成字符串类型,使用 “/” 进行分割,然后取最后一部分的内容赋给 called_name 。这样说起来可能比较抽象,所以我把 inspect.stack() 打印出来了,如下:

 

 

温故而知新
      – 列表
      – 元组
  
可以尝试输出对应的测试方法名称,如上~~ 

 

Tags: