最近因项目需要重新拾起了 UI 自动化,正好试用了下 Airtest,感觉开发脚本的效率确实高了许多,调试起来也很方便,但是不支持脚本批量执行这就非常不得劲了,所以那句老话就来了:“自己动手,丰衣足食”。
Airtest 执行脚本的核心代码位于airtest\cli\runner.py
中,大体流程如下:
根据上面的流程分析一下,run_script
方法肯定要修改以便加入多个用例;在每次用例执行前我们都需要修改工作目录,因此setUp
也需要修改。既然修改点明确了,那么我们来梳理一下思路:
run_script
方法中先遍历目录获取出所有用例,然后统一添加都 suite 中setUp
方法中调用auto_setup
方法设置日志路径,实现结果分开存放setUp
方法中需要知道每个脚本的日志路径,所以要为 AirtestCase 添加一个属性保存日志路径并在run_script
方法中为其设置对应的值至此执行部分分析完成
Airtest 生成报告的核心代码位于airtest\report\report.py
中,大体流程如下:
因为我们的日志是分开存放的,要给每个日志都生成一个 html 报告只需在main
方法里加个循环,遍历所有日志即可;生成聚合报告这里有点麻烦,因为需要获取 LogToHtml 内部的test_result
属性,但是 Python 的动态特性允许我们使用 types 动态的为实例绑定方法,所以我们可以定义一个方法返回test_result
属性,然后绑定给实例就可以了;最后自定义一个模板,把收集到的结果通过 jinja2 渲染进去,大功告成!
项目目录结构
生成的报告目录结构
聚合报告
核心的逻辑在 runner.py 中,可根据自己的实际情况进行修改,目前 runner.py 中的逻辑如下:
.py
结尾的文件,会忽略以 “__” 开头的缓存文件)添加到测试集runCase
方法,所以需要将测试代码封装成runCase
方法,签名如下runCase(self, vars)
,代码位置
欢迎大家多提宝贵意见,谢谢
就酱,上个链接,溜了溜了。。。(o゚v゚) ノ
airetest 并行的时候,如果对每个测试用例都调用 run_script,不同的测试用例参数不一样(比如脚本路径、日志路径等),而 run_script 中会设置 Settings 全局变量,这样难道不会有冲突吗
请问一下 airtest 升级后新报告样式有适配吗?目前新版本的报告聚合后图片显示不出来呢
优化了下测试报告
你是在编译器打开的,编译器基于项目给一个 ip,不显示图片是因为浏览器禁止访问本地资源!
你只需在文件中双击打开此文件就会正常显示了!!!
首先我没用这里楼主的方法,而是百度的时候,发现其他大牛有不需要修改的源码的方法,大同小异。然后说这个问题,我调试半天发现路径里面 就是以\结尾,后来断点调试,发现是 airtest 的源码里面,对运行目录和脚本名字进行了拼接,而脚本名字又默认是空,导致拼接后就以\结尾了,这个路径系统是不认的,所以我尝试改了一下,直接不拼接了,,,没想到运行通过了……
具体就是 修改了源码 report 模块下面一行代码(不想修改源码,最后还是改了),具体截图如下
日志下面有图片,格式也是正确的,但是在报告里面不显示截图,这个是什么情况呢?
请问一下,为什么聚合后的报告没有图片?
借问高手
报错是因为 args 找不到,这个是在 run_script 方法中定义的全局变量,直接把 runner.py 当主类启动.
请问下大佬 , 在执行 runner 时报 NameError: name 'args' is not defined 的错
报错是因为 args 找不到,这个是在 run_script 方法中定义的全局变量,直接把 runner.py 当主类启动
请问下,我已经封装成了 runcase 方法,但是执行报错,是什么原因
rpt.report("log_template.html", output_file=output_file)
在执行上面一句时,报错
有人遇过这种情况吗
Airtest/tests/test_cli.py 这个也行,基于 unittest
你这个就只是修改了它原有的 runner 方法吧?
需要封装成runCase
方法
# -*- coding: utf-8 -*-
author = "qiuyunxia"
from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
def runCase(self, vars):
auto_setup(file)
poco = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False)
dev=connect_device("android:///988bdc454835315257")
dev.stop_app("com.unity3d.ads.example.creative")
dev_list =device()
print(dev_list.list_app())
dev.start_app("com.unity3d.ads.example.creative")
poco("com.unity3d.ads.example.creative:id/unityads_example_initialize_button").click()
snapshot()
poco("com.unity3d.ads.example.creative:id/unityads_example_interstitial_button").click()
sleep(4.0)
snapshot()
snapshot()
wait(Template(r"tpl1547966723580.png", record_pos=(0.005, 0.828), resolution=(1080, 2220)))
touch(Template(r"tpl1547966742057.png", record_pos=(0.005, 0.847), resolution=(1080, 2220)))
sleep(1.0)
snapshot()
我用下面的 command 生成出 EXPORT
report airtest report xxx.air --export EXPORT
但是实际看到的 log.html 却只有纪录着某次跑只有一分钟的 behavior
我已经跑测试跑了一整天
有人知道为什么吗?
请教下 case 内的 py 脚本有啥要求
author = "qiuyunxia"
from airtest.core.api import *
auto_setup(file)
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
poco = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False)
dev=connect_device("android:///988bdc454835315257")
dev.stop_app("com.unity3d.ads.example.creative")
dev_list =device()
print(dev_list.list_app())
dev.start_app("com.unity3d.ads.example.creative")
poco("com.unity3d.ads.example.creative:id/unityads_example_initialize_button").click()
snapshot()
poco("com.unity3d.ads.example.creative:id/unityads_example_interstitial_button").click()
sleep(4.0)
snapshot()
snapshot()
wait(Template(r"tpl1547966723580.png", record_pos=(0.005, 0.828), resolution=(1080, 2220)))
touch(Template(r"tpl1547966742057.png", record_pos=(0.005, 0.847), resolution=(1080, 2220)))
sleep(1.0)
snapshot()
类似这样子不可以?
运行一直显示 case 为 0,我是用的 air 作为后缀查找的,麻烦大神帮看下
runner.py 会自动查找跟它在同一目录下且名字以 “用例集” 结尾的文件夹,然后将找到的文件夹里的 py 文件加入都测试用例集中,你那找不到用例查一下是不是因为编码问题导致的
大神不敢当,当时写代码是确实是往这方面考虑的,但是最近比较忙可能抽不出时间来完善,如果你有好的想法或实现欢迎 PR
需要手动调下结构,其实不调也能运行,需要把好多 air 的脚本和图片都放到一个目录里,这样很容易造成图片不知道是哪个脚本的,太乱;如果想要直接运行 air 脚本可以把 runner.py 里遍历文件的逻辑改下就行了
请问下,该框架是否支持 window 应用的脚本呢?试过安卓 APP 脚本,是可以正常运行的,但执行 windows 应用时,报错了,报错信息如下:
这个文件夹结构需要自己创建么,最好新建脚本的时候就能建好这个结构。。。这一步做了就比较方便了
大佬,想问一下和用命令行或者 shell 脚本批量运行有什么区别么?我现在是这么实现的
另外聚合报告这个挺好的,正缺这个
airtest 是什么? 好用么?我一直用 selenium
我刚拿 poco 试了一下是没问题,但是我用的是 Python 3.6.5,不知道你那报错是不是因为你用 Python 2.7 的原因
我看你文件夹里确实没有那个文件啊,而且命令行里输出的图片是 tpl 开头的,你的日志文件夹里的图片都没有 tpl 前缀并且是 jpg 格式的。这一块在我写的 report.py 里我并没有做修改而是直接调用的原来的逻辑,我也不知道图片名称为啥出现这种差异
那个_analyse 方法我没改,直接调用的 airtest 框架原来的,要不你自己改下 airtest/aircv/aircv.py(就是异常堆栈里的最后一个文件),把那个 filename 变量输出出来看看到底是什么值
参考你的方案做了一个新的方法出来,感谢大牛分享
runner.py 改成下面的代码就不用封装方法了
# -*- coding: utf-8 -*-
import unittest
import os
import sys
import six
import traceback
import types
import time
from io import open
from airtest.cli.parser import runner_parser
from airtest.core.api import G, auto_setup, log
from airtest.core.settings import Settings as ST
from airtest.utils.compat import decode_path
from copy import copy
class MyAirtestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.args = args
setup_by_args(args)
# setup script exec scope
cls.scope = copy(globals())
def setUp(self):
if self.args.log and self.args.recording:
for dev in G.DEVICE_LIST:
try:
dev.start_recording()
except:
traceback.print_exc()
# 设置日志路径
auto_setup(logdir=self._logdir)
def tearDown(self):
if self.args.log and self.args.recording:
for k, dev in enumerate(G.DEVICE_LIST):
try:
output = os.path.join(self.args.log, "recording_%d.mp4" % k)
dev.stop_recording(output)
except:
traceback.print_exc()
def runTest(self):
try:
# 调用脚本中的runCase方法并传递scope供脚本使用
exec(self._code["code"], self._code["ns"])
except Exception as err:
tb = traceback.format_exc()
log("Final Error", tb)
six.reraise(*sys.exc_info())
@property
def logdir(self):
return self._logdir
@logdir.setter
def logdir(self, value):
self._logdir = value
@property
def code(self):
return self._code
@code.setter
def code(self, value):
self._code = value
def setup_by_args(args):
# init devices
if isinstance(args.device, list):
devices = args.device
elif args.device:
devices = [args.device]
else:
devices = []
print("do not connect device")
# set base dir to find tpl
args.script = decode_path(args.script)
# set log dir
if args.log is True:
print("save log in %s/log" % args.script)
args.log = os.path.join(args.script, "log")
elif args.log:
print("save log in '%s'" % args.log)
args.log = decode_path(args.log)
else:
print("do not save log")
# guess project_root to be basedir of current .air path
project_root = os.path.dirname(args.script) if not ST.PROJECT_ROOT else None
# 此处不设置日志路径,防止生成多余的log.txt
auto_setup(args.script, devices, None, project_root)
def new_case(py, logdir):
"""实例化MyAirtestCase并绑定runCase方法"""
with open(py, 'r', encoding="utf8") as f:
code = f.read()
obj = compile(code.encode("utf-8"), py, "exec")
ns = {}
ns["__file__"] = py
case = MyAirtestCase()
pyfilename = os.path.basename(py).replace(".py", "")
# 设置属性以便在setUp中设置日志路径
case.logdir = os.path.join(logdir, pyfilename)
case.code = {"code": obj, "ns": ns}
return case
def init_log_folder():
"""初始化日志根目录"""
name = time.strftime("log_%Y%m%d_%H%M%S", time.localtime())
if not os.path.exists(name):
os.mkdir(name)
return name
def run_script(parsed_args, testcase_cls=MyAirtestCase):
global args # make it global deliberately to be used in MyAirtestCase & test scripts
args = parsed_args
dir = os.path.dirname(os.path.realpath(__file__))
suites = []
pys = []
# 获取所有用例集
for f in os.listdir(dir):
if f.endswith("air"):
f = os.path.join(dir, f)
if os.path.isdir(f):
suites.append(f)
# 获取所有脚本
for s in suites:
for f in os.listdir(s):
if f.endswith(".py") and not f.startswith("__"):
pys.append(os.path.join(s, f))
logdir = os.path.join(dir, init_log_folder())
args.log = logdir
suite = unittest.TestSuite()
# 添加脚本
for py in pys:
case = new_case(py, logdir)
suite.addTest(case)
result = unittest.TextTestRunner(verbosity=0).run(suite)
if not result.wasSuccessful():
sys.exit(-1)
if __name__ == "__main__":
ap = runner_parser()
args = ap.parse_args()
run_script(args, MyAirtestCase)
报错是因为需要将测试代码封装成一个 runCase 方法,像下面那样
def runCase(self, vars):
# 业务逻辑代码
pass
代码在这里 new_case 方法
之所以这么整是考虑后面要实现数据驱动,可以通过方法签名里那个 vars 参数来传递数据,这样看起来比直接在命名空间里获取数据稍微清晰一点
赞一个!回去试下
请问一下这个是直接可以用的吗?为什么我把我的脚本放进去执行都是显示 0 个执行了