个人感觉使用unittest维护测试用例,使用HTMLTestRunner生成测试报告很不方便,于是自己写了一个接口自动化,使用excel维护接口测试用例,简单明了,测试完成后生成html测试报告,并发送邮件。
项目结构如下:
总体思路:
1、用excel维护测试用例;
2、读取excel中的测试用例,按顺序执行各接口;
3、判断接口响应值,如果接口请求失败,或响应值不正确,则将错误信息记录下来;
4、将测试结果写到一个html里;
5、将测试结果通过邮件发送;
6、可以设置周期执行、定时执行、被测系统重启后自动执行。
项目配置文件,项目相关的配置如下:
# 是否在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')
程序入口,控制程序运行方式,包括单次执行、周期执行、定时执行和服务重启后立即执行,相关配置如下:
# 定时任务设置
# 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
存放测试用例和全局变量,其中globalVariables.txt
如下:
username root
password 1234567
注:变量名和变量值之间必须有空格,且每行只能有一条数据
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里
保存测试结果,和日志文件
用于预期结果和接口响应值比较。
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
用于从数据库中读取数据,以参数化接口传参。该函数需要根据实际情况更改,如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]})
用于发送邮件,相关配置如下,邮件账号登陆密码需要加密。
# 测试完成后是否自动发送邮件
IS_EMAIL = True
# 邮箱配置,qq邮箱为smtp.qq.com
# 所用的发件邮箱必须开启SMTP服务
SMTP_SERVER = 'smtp.sina.com'
# 发件人
SENDER_NAME = '张三'
SENDER_EMAIL = '[email protected]'
# 邮箱登陆密码,经过base64编码
PASSWORD = 'UjBWYVJFZE9RbFpIV1QwOVBUMDlQUT09'
# 收件人,对应 baidu_all.txt 文件,该文件为邮件组名。
RECEIVER_NAME = 'baidu_all'
# RECEIVER_EMAIL = 'baidu_all.txt' 多个收件人用英文逗号分隔
RECEIVER_NAME = 'baidu_all’为邮件上显示的收件人,通常公司群发邮件的收件人为邮件组的名字,而邮件组里包含很多个人的邮箱地址。因此收件人邮箱地址配置在邮件组名对应的txt文件里,如baidu_all.txt,多个收件人用英文逗号隔开。
和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
文件中先输入密码,执行完成后生成加密后的字符串,将该字符串配置到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))
用于读取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) # 直接将请求参数放到接口中
用于生成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
发起请求。目前仅支持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)
主要用于处理globalVariables.txt
文件
所有测试操作都在这里进行。
项目地址:https://github.com/leeyoshinari/ATI
欢迎大家提出改进意见。