个人感觉使用unittest维护测试用例,使用HTMLTestRunner生成测试报告很不方便,于是自己写了一个接口自动化,使用excel维护接口测试用例,简单明了,测试完成后生成html测试报告,并发送邮件。

ATI(Automated Testing of Interfaces)

项目结构如下:

python接口自动化如何上传文件_用例



总体思路:

1、用excel维护测试用例;

2、读取excel中的测试用例,按顺序执行各接口;

3、判断接口响应值,如果接口请求失败,或响应值不正确,则将错误信息记录下来;

4、将测试结果写到一个html里;

5、将测试结果通过邮件发送;

6、可以设置周期执行、定时执行、被测系统重启后自动执行。

config.py

项目配置文件,项目相关的配置如下:

# 是否在Linux上使用,0为在Windows上使用,1为在Linux上使用
IS_LINUX = 1
# 日志级别
LOG_LEVEL = 'INFO'
# 接口响应超时时间
TIMEOUT = 0.5
# 检查端口是否存在间隔时间
SLEEP = 60

# ip地址和端口
IP = '127.0.0.1'
PORT = '8888'
# 请求头
HEADERS = {}

# 测试用例路径
TESTCASE_PATH = os.path.join(os.path.dirname(__file__), 'testCase', 'testCase.xlsx')
# 全局变量路径
GLOBAL_VARIABLES = os.path.join(os.path.dirname(__file__), 'testCase', 'globalVariables.txt')
# 测试结果存放路径
RESULT_PATH = os.path.join(os.path.dirname(__file__), 'result')
# 日志路径
LOG_PATH = os.path.join(os.path.dirname(__file__), 'result')

main.py

程序入口,控制程序运行方式,包括单次执行、周期执行、定时执行和服务重启后立即执行,相关配置如下:

# 定时任务设置
# 0为只执行一次,1为每隔INTERVAL(单位s)执行一次,2为每天TIMER_SET执行一次
# 在Linux和Windows上均可以设置为0,1和2仅对Linux上有效
QUERY_TYPE = 0
# 执行间隔时间,单位为秒
INTERVAL = 120
# 定时任务执行时间
TIMER_SET = '23:59:00'
# 服务重启后是否执行。如果服务重新启动,则立即执行,仅QUERY_TYPE为1或2时有效,如果QUERY_TYPE为1,INTERVAL将重新计算
IS_START = True

testCase

存放测试用例和全局变量,其中globalVariables.txt如下:

username root
password 1234567

注:变量名和变量值之间必须有空格,且每行只能有一条数据

testCase.xlsx为测试用例,详细如下:

python接口自动化如何上传文件_html_02

1、用例ID:用例ID中的数字后续生成测试报告有用到,用于控制每行颜色;
2、用例名称:用例名称;
3、是否执行:控制该用例是否执行,0为不执行,1为执行;
4、优先级:暂未使用;
5、请求接口:请求接口全路径,拼接结果 http://IP:PORT/eds/login;
6、协议:包括 http 和 https ;
7、请求参数:主要用于post请求传参,如果传固定值,可直接写固定值,如果是变量,需要加‘<>’号;
   如果是get请求中的url需要参数化,则需要在请求接口中变量的位置加‘{}’号,相对应的请求参数需要写参数值,同样用‘<>’号,如果需要传多个参数,用英文逗号‘,'隔开;
8、获取响应值中的某个字段的值:如果需要获取当前接口响应值中的某个字段的值,作为后面接口的输入参数,例如:
   如果返回值为{"code":0,"msg":"success","data":1},如需获取data字段的值,则直接写data就可以了;
9、变量名:获取响应值中的某个字段的值后,存储的变量名,相同的变量名的值会覆盖;
10、接口响应超时:接口响应超时时间,如果为0或为空,则默认为配置文件里配置的时间,单位为秒;
11、预期结果 和 断言内容 :如果接口的响应值包括断言内容,则可以认为接口测试通过;
    如果断言内容为空,则会比较 预期结果 和接口真实响应值,预期结果必须为json;
    预期结果 和接口真实响应值比较时,会根据预期结果中的字段,比较接口真实响应值中对应字段的值,可以只比较部分字段;
12、测试结果:是否执行成功;
13、失败原因:可能失败原因,即程序运行过程中报错信息;
14、备注

注:不同功能的接口可以放在不同的sheet里

result

保存测试结果,和日志文件

common

compare.py

用于预期结果和接口响应值比较。

def compare(self, new_json, raw_json):
   """
      比较两个json是否相等
      遍历 new_json 中的key和value,然后在 raw_json 中找对应字段中的值,判断是否相等,
      如果key在new_json中,但不在raw_json中,会报错;如果key在raw_json中,但不在new_json中,不会报错
   """
   if isinstance(new_json, dict) and isinstance(raw_json, dict):
      self.parser_dict(new_json, raw_json)
   elif isinstance(new_json, list) and isinstance(raw_json, list):
      self.parser_list(new_json, raw_json)
   else:
      self.flag = 0
      self.reason = 'Type error'

   return self.flag, self.reason
DatabaseController.py

用于从数据库中读取数据,以参数化接口传参。该函数需要根据实际情况更改,如sql脚本和变量名。默认为不使用数据库中读取数据。

def read_data_from_mysql(self):
   """
      从MySQL数据库中读取数据
   """
   import pymysql
   db = pymysql.connect(cfg.MYSQL_IP, cfg.MYSQL_USERNAME, cfg.MYSQL_PASSWORD, cfg.MYSQL_DATABASE)
   cursor = db.cursor()

   sql = 'select name, password from user order by rand() limit 1;'
   cursor.execute(sql)
   res = cursor.fetchall()
   for i in range(len(res)):
      self.varibles.update({self.variable_name[i]: res[i][0]})
EmailController.py

用于发送邮件,相关配置如下,邮件账号登陆密码需要加密。

# 测试完成后是否自动发送邮件
IS_EMAIL = True
# 邮箱配置,qq邮箱为smtp.qq.com
# 所用的发件邮箱必须开启SMTP服务
SMTP_SERVER = 'smtp.sina.com'
# 发件人
SENDER_NAME = '张三'
SENDER_EMAIL = 'zhangsan@qq.com'
# 邮箱登陆密码,经过base64编码
PASSWORD = 'UjBWYVJFZE9RbFpIV1QwOVBUMDlQUT09'
# 收件人,对应 baidu_all.txt 文件,该文件为邮件组名。
RECEIVER_NAME = 'baidu_all'
# RECEIVER_EMAIL = 'baidu_all.txt'    多个收件人用英文逗号分隔

RECEIVER_NAME = 'baidu_all’为邮件上显示的收件人,通常公司群发邮件的收件人为邮件组的名字,而邮件组里包含很多个人的邮箱地址。因此收件人邮箱地址配置在邮件组名对应的txt文件里,如baidu_all.txt,多个收件人用英文逗号隔开。

PwdEncrypt.pyc

PwdEncrypt.py文件一样,区别是编译成pyc文件,但是仍可以反编译出来,pyc文件并不安全。这里随意写的加密方法,可更改。emailServer函数用于解密并登陆邮箱。

def emailServer(smtp_server, port, username, password):
   def dencrypt(pwd):
      s5 = base64.b64decode(pwd)
      s4 = base64.b85decode(s5)
      s4 = s5
      s3 = base64.b64decode(s4)
      s2 = base64.b64decode(s5)
      s3 = s2
      s1 = base64.b32decode(s3)
      return s1.decode()

   server = smtplib.SMTP_SSL(smtp_server, port)
   server.login(username, dencrypt(password))
   return server
GenerateKey.py

主要用于加密密码,请使用其他安全度较高的加密方式。在GenerateKey.py文件中先输入密码,执行完成后生成加密后的字符串,将该字符串配置到config.py配置文件中。

def encrypt(pwd):
   s1 = base64.b32encode(pwd.encode())
   s2 = base64.b64encode(s1)
   s1 = s2
   s3 = base64.b16encode(s2)
   s3 = s1
   s4 = base64.b85encode(s2)
   s5 = base64.b64encode(s3)
   return s5.decode()

password = '123456'
print(encrypt(password))
ExcelController.py

用于读取excel文件中的测试用例。

excel = xlrd.open_workbook(self.path)   # 打开excel表格
sheets = excel.sheet_names()        # 获取excel中所有的sheet
for sheet in sheets:
   table = excel.sheet_by_name(sheet)      # 获取sheet中的单元格
   for i in range(1, table.nrows):     # 遍历所有非空单元格
      if table.cell_value(i, 0):      # 用例ID非空
         caseId = table.cell_value(i, 0).strip()     # 用例ID

         if not int(table.cell_value(i, 2)):
            logger.logger.info('用例Id {} 不执行,已跳过'.format(caseId))
            continue

         caseName = table.cell_value(i, 1).strip()
         priority = int(table.cell_value(i, 3))
         interface = table.cell_value(i, 4).strip()
         protocol = table.cell_value(i, 5)
         method = table.cell_value(i, 6)
         data = self.compile(table.cell_value(i, 7))
         key = table.cell_value(i, 8)
         name = table.cell_value(i, 9)
         timeout = table.cell_value(i, 10)
         expectedResult = table.cell_value(i, 11)
         assertion = table.cell_value(i, 12).strip()

         if method == 'get' and data:    # 如果是get请求,且有请求参数
            request_data = data.split(',')
            interface = interface.format(*request_data)     # 直接将请求参数放到接口中
HtmlController.py

用于生成html格式的测试报告。

def writeHtml(self):
   """
      生成html测试报告
   """
   fail_case_num = len(self._fail_case)    # 失败用例数
   all_case_num = len(self._all_case)      # 所有用例数
   success_rate = (1 - fail_case_num / all_case_num) * 100     # 计算成功率
   spend_time = time.time() - self.start_time      # 测试花费总时间
   # 把所有用例连接起来,拼成完整的表格
   fail_rows = ''.join(self._fail_case)
   all_rows = ''.join(self._all_case)
   # 失败用例表格和所有用例表格分开保存
   fail_table = self.table.format('{}{}'.format(self.table_head, fail_rows))
   all_table = self.table.format('{}{}'.format(self.table_head, all_rows))
   # 测试结果概览详情
   detail = self.overview1.format(all_case_num, spend_time, all_case_num-fail_case_num, fail_case_num, success_rate)
   # 测试报告标题
   header = '{}{}{}{}'.format(self.title, self.test_time, self.overview, detail)
   # 根据成功率决定邮件正文内容,生成失败用例测试报告
   if success_rate == 100:
      fail_html = self.html.format('{}{}{}'.format(header, self.fail2, self.last))
   else:
      fail_html = self.html.format('{}{}{}{}'.format(header, self.fail1, fail_table, self.last))
   # 生成所有用例测试报告
   all_html = self.html.format('{}{}{}'.format(header, self.success, all_table))
   # 将所有用例测试报告保存到本地
   html_path = os.path.join(self.path, self.name + '.html')
   with open(html_path, 'w') as f:
      f.writelines(all_html)

   logger.logger.info('所有用例测试结果保存成功。')
   return fail_html, self.name

测试报告长的是这个样子的

python接口自动化如何上传文件_json_03

request.py

发起请求。目前仅支持get和post请求,其他请求可自行增加。

def request(self, method, protocol, interface, data, headers=None, timeout=None):
   """请求入口,目前仅支持get和post请求,其他请求可自行添加"""
   if timeout is None:
      timeout = cfg.TIMEOUT
   if headers is None:
      headers = cfg.HEADERS       # 需要请求头

   try:
      if method == 'get':
         res = self.get(protocol, interface, timeout)
      elif method == 'post':
         res = self.post(protocol, interface, data, headers, timeout)
      else:
         logger.logger.error('暂不支持其他请求方式')
         raise Exception('暂不支持其他请求方式')
      return res
   except Exception as err:
      logger.logger.error(err)
      raise Exception(err)
TxtToDict.py

主要用于处理globalVariables.txt文件

Testing.py

所有测试操作都在这里进行。