前面的文章介绍了使用 bat 执行用例,后面我也说过不会使用这种方式,因为,生成的用例报告都是单个的,不利于整体观察,所以有了今天的要写的内容,如何生成聚合报告。其实,这个原理是很简单的,就是将之前的单用例报告地址添加到一个新开发的 html页面中。最终的效果如下图:有用例数、通过率、设备信息、每个用例在不同的设备上执行情况、查看详情。下面,逐步展开讲解。
在讲之前,先划分好项目目录,这个比较重要,在项目根目录创建三个目录:case(专门用来保存测试用例)、log(保存日志,以及单个测试用例报告)、result(存放单个用例在不同设备的执行状态以及报告地址)。我们要在多机执行用例,那就要获取设备信息,这是第一步要做的事情,Airtest 有个 ADB 模块,使用该模块的方法获取:[ dev[0] for dev in ADB().devices() ],返回一个设备列表,先放这里,后面再用。然后,在获取测试用例,其实就是遍历保存用例的目录,将其加入到列表中。接下来就是执行主程序了,把设备列表、用例列表传递进去,循环获取用例,里面嵌套循环设备,让用例在每个设备上执行,这个点就是拼接 Airtest 用例执行命令,然后通过管道执行 cmd 命令并记录结果。下面通过代码的形式进行讲解,会更加直观。
import traceback
import subprocess
import webbrowser
import time
import json
import shutil
from airtest.core.android.adb import ADB
from jinja2 import Environment, FileSystemLoader
# 主程序入口
def run(devices, cases):
"""
先清除以前的log、result
"""
report_log = get_report_dir()
if os.path.isdir(report_log):
shutil.rmtree(report_log)
os.mkdir(report_log)
log_dir = os.path.join(os.getcwd(), 'log')
if os.path.isdir(log_dir):
shutil.rmtree(log_dir)
try:
data_r = []
global time_s
time_s = time.time()
for case in cases:
results = load_json_data(case)
tasks = run_on_multi_device(devices, case, results)
for task in tasks:
status = task['process'].wait()
results['tests'][task['dev']] = run_one_report(task['case'], task['dev']) # {'status': -1, 'device': dev, 'path': ''}
results['tests'][task['dev']]['status'] = status
name = case.split(".")[0]
file = os.path.join(report_log, name + "_data.json")
json.dump(results, open(file, "w"), indent=4)
data_r.append(results)
print(data_r)
run_summary(data_r)
except Exception as e:
traceback.print_exc()
# 在多台设备上运行airtest 脚本
def run_on_multi_device(devices, case, results):
tasks = []
for dev in devices:
log_dir = get_log_dir(case,dev)
print("执行脚本,保存日志路径====>" + str(log_dir))
# 这里进行了参数化操作,通过 --mobile 获取传入的参数,但是这个参数要添加到 runner_parser 方法中
if dev == "ce091719b82fc830027e":
phone = "15511416644"
elif dev == "AYK4C17C13003612":
phone = "17703719999"
# 命令行执行:airtest run xxx.air --device Android://127.0.0.1:5037/b7f0c036 --log xxx/log/xxx.log/xxxxxxxx/
cmd = [ "airtest", "run", os.path.join(os.getcwd(),"case", case), "--device", "Android:///" + dev, "--log", log_dir, "--mobile",phone]
try:
tasks.append({
'process': subprocess.Popen(cmd, cwd=os.getcwd()),
'dev': dev,
'case': case
})
except Exception as e:
traceback.print_exc()
return tasks
# 生成单个用例测试报告, 就是点击每个用例的详情页面
def run_one_report(case, dev):
try:
log_dir = get_log_dir(case,dev)
log = os.path.join(log_dir, 'log.txt')
print("生成报告,日志读取日志路径===>>" + str(log_dir))
if os.path.isfile(log):
cmd = [ "airtest", "report", os.path.join(os.getcwd(),"case", case), "--log_root", log_dir, "--outfile", os.path.join(log_dir, 'log.html'), "--lang", "zh" ]
ret = subprocess.call(cmd, shell=True, cwd=os.getcwd())
return {
'status': ret,
# 这里使用的相对路径,目的是便于移植,这里的结构是 log/用例名字/设备/报告.html
'path': os.path.join(".\\log", case.replace(".air",".log"),dev,'log.html') # os.path.join(os.getcwd(), "log", "follow.log", "log.html") 查看详细报告的相对路径
}
else:
print("Report build Failed. File not found in dir %s" % log)
except Exception as e:
traceback.print_exc()
return {'status': -1, 'device': dev, 'path': ''}
# 加载进度 返回一个空的进度数据
def load_json_data(case):
return {
'start': time.time(),
'script': case,
'tests': { }
}
# 生成汇总的测试报告
def run_summary(data):
try:
for dt in data:
res = get_value_by_key(dt, "status")
summary = {
'time': "%.3f" % (time.time() - time_s),
'success': res.count(0),
'count': len(res)
}
summary['start_all'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time_s))
summary["result"] = data
print("summary ====>", summary)
# 聚合报告模板,自己定义开发,也可以借用我的,需要的找我要
env = Environment(loader=FileSystemLoader(os.getcwd()), trim_blocks=True)
html = env.get_template('report_template.html').render(data=summary)
with open("result.html", "w", encoding="utf-8") as f:
f.write(html)
# 执行完毕打开聚合报告,可以关闭
webbrowser.open("result.html")
except Exception as e:
traceback.print_exc()
# 获取 key 为 status 的值
def get_value_by_key(in_json, target_key,results=[]):
for key, value in in_json.items(): # 循环获取key,value
if key == target_key:
results.append(value)
if isinstance(value, dict):
get_value_by_key(value, target_key)
return results
# 获取路径下所有air的测试用例文件
def get_cases():
cases = []
for name in os.listdir(os.path.join(os.getcwd(),"case")): # 遍历当前路径下的文件夹和文件名称
if name.endswith(".air"):
cases.append(name)
return cases
# 用例-设备 日志目录
def get_log_dir(case, device=None):
log_dir = os.path.join(os.getcwd(), 'log', case.replace(".air", ".log"), device.replace(".", "_").replace(':', '_'))
if not os.path.isdir(log_dir):
os.makedirs(log_dir)
return log_dir
# 测试报告目录
def get_report_dir():
report_path = os.path.join(os.getcwd(), "result")
if not os.path.isdir(report_path):
os.mkdir(report_path)
return report_path
def sort_cases(cases, loginAir, outAir):
# 清除列表中的登录、退出登录,然后将其分别添加到列表的第一位和最后一位
cases.remove(loginAir)
cases.remove(outAir)
cases.insert(0, loginAir)
cases.insert(len(cases), outAir)
return cases
if __name__ == '__main__':
"""
初始化数据
"""
# 获取所有已连接的设备列表
devices = [tmp[0] for tmp in ADB().devices()]
# 设置指定设备执行测试用例
# devices = ["BTY4C16705003852","b7f0c036"]
# 获取所有测试用例
cases = get_cases()
# 将登录用例排在最前面执行,退出用例排在最后面执行
#sort_airs = sort_cases(airs, "loginPro.air", "loginOutPro.air")
# 获取指定用例,按顺序执行
# sort_airs = ["openCardPro.air","openOrderPro.air","quickMoneyPro.air"]
"""
执行脚本
"""
# 运行所有脚本
run(devices, cases)
到这里,基本上就ok了,后面大家可以根据自己需要,将报告移到指定的服务器目录,将报告通过邮件、钉钉等发送出去,点击连接就可以看到全部结果,我这里省略了。里面用到的聚合报告的模板,如果需要可以找我要,也可以让前端开发一个。到此,Airtest 告一段落,后面遇到特殊的情况,再向大家分享。