最近因项目需要重新拾起了 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゚) ノ
请问一下这个是直接可以用的吗?为什么我把我的脚本放进去执行都是显示 0 个执行了
赞一个!回去试下
报错是因为需要将测试代码封装成一个 runCase 方法,像下面那样
def runCase(self, vars):
# 业务逻辑代码
pass
代码在这里 new_case 方法
之所以这么整是考虑后面要实现数据驱动,可以通过方法签名里那个 vars 参数来传递数据,这样看起来比直接在命名空间里获取数据稍微清晰一点
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)
参考你的方案做了一个新的方法出来,感谢大牛分享
那个_analyse 方法我没改,直接调用的 airtest 框架原来的,要不你自己改下 airtest/aircv/aircv.py(就是异常堆栈里的最后一个文件),把那个 filename 变量输出出来看看到底是什么值
我看你文件夹里确实没有那个文件啊,而且命令行里输出的图片是 tpl 开头的,你的日志文件夹里的图片都没有 tpl 前缀并且是 jpg 格式的。这一块在我写的 report.py 里我并没有做修改而是直接调用的原来的逻辑,我也不知道图片名称为啥出现这种差异
我刚拿 poco 试了一下是没问题,但是我用的是 Python 3.6.5,不知道你那报错是不是因为你用 Python 2.7 的原因
airtest 是什么? 好用么?我一直用 selenium
大佬,想问一下和用命令行或者 shell 脚本批量运行有什么区别么?我现在是这么实现的
另外聚合报告这个挺好的,正缺这个
这个文件夹结构需要自己创建么,最好新建脚本的时候就能建好这个结构。。。这一步做了就比较方便了
请问下,该框架是否支持 window 应用的脚本呢?试过安卓 APP 脚本,是可以正常运行的,但执行 windows 应用时,报错了,报错信息如下:
需要手动调下结构,其实不调也能运行,需要把好多 air 的脚本和图片都放到一个目录里,这样很容易造成图片不知道是哪个脚本的,太乱;如果想要直接运行 air 脚本可以把 runner.py 里遍历文件的逻辑改下就行了
大神不敢当,当时写代码是确实是往这方面考虑的,但是最近比较忙可能抽不出时间来完善,如果你有好的想法或实现欢迎 PR
runner.py 会自动查找跟它在同一目录下且名字以 “用例集” 结尾的文件夹,然后将找到的文件夹里的 py 文件加入都测试用例集中,你那找不到用例查一下是不是因为编码问题导致的
运行一直显示 case 为 0,我是用的 air 作为后缀查找的,麻烦大神帮看下
请教下 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()
类似这样子不可以?
我用下面的 command 生成出 EXPORT
report airtest report xxx.air --export EXPORT
但是实际看到的 log.html 却只有纪录着某次跑只有一分钟的 behavior
我已经跑测试跑了一整天
有人知道为什么吗?
需要封装成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()
你这个就只是修改了它原有的 runner 方法吧?
Airtest/tests/test_cli.py 这个也行,基于 unittest
rpt.report("log_template.html", output_file=output_file)
在执行上面一句时,报错
有人遇过这种情况吗
请问下,我已经封装成了 runcase 方法,但是执行报错,是什么原因
报错是因为 args 找不到,这个是在 run_script 方法中定义的全局变量,直接把 runner.py 当主类启动
请问下大佬 , 在执行 runner 时报 NameError: name 'args' is not defined 的错
报错是因为 args 找不到,这个是在 run_script 方法中定义的全局变量,直接把 runner.py 当主类启动.
借问高手
请问一下,为什么聚合后的报告没有图片?
日志下面有图片,格式也是正确的,但是在报告里面不显示截图,这个是什么情况呢?
首先我没用这里楼主的方法,而是百度的时候,发现其他大牛有不需要修改的源码的方法,大同小异。然后说这个问题,我调试半天发现路径里面 就是以\结尾,后来断点调试,发现是 airtest 的源码里面,对运行目录和脚本名字进行了拼接,而脚本名字又默认是空,导致拼接后就以\结尾了,这个路径系统是不认的,所以我尝试改了一下,直接不拼接了,,,没想到运行通过了……
具体就是 修改了源码 report 模块下面一行代码(不想修改源码,最后还是改了),具体截图如下
你是在编译器打开的,编译器基于项目给一个 ip,不显示图片是因为浏览器禁止访问本地资源!
你只需在文件中双击打开此文件就会正常显示了!!!
优化了下测试报告
请问一下 airtest 升级后新报告样式有适配吗?目前新版本的报告聚合后图片显示不出来呢
airetest 并行的时候,如果对每个测试用例都调用 run_script,不同的测试用例参数不一样(比如脚本路径、日志路径等),而 run_script 中会设置 Settings 全局变量,这样难道不会有冲突吗