Airtest Airtest 多脚本执行及报告聚合

xinufo · October 23, 2018 · Last by jiawei.li replied at June 11, 2019 · 5307 hits

最近因项目需要重新拾起了UI自动化,正好试用了下Airtest,感觉开发脚本的效率确实高了许多,调试起来也很方便,但是不支持脚本批量执行这就非常不得劲了,所以那句老话就来了:“自己动手,丰衣足食”。

需求

  1. 脚本批量执行
  2. 每个脚本执行日志分开存放
  3. 每个脚本单独生成一个html报告并在父文件夹生成一个聚合报告

Airtest 执行脚本流程分析

Airtest 执行脚本的核心代码位于airtest\cli\runner.py中,大体流程如下:

  1. 解析命令行参数
  2. 实例化 AirtestCase 并添加到 suite 中
  3. 控制权交给 unittest 执行测试
  4. 由 unittest 调用 AirtestCase 的 runTest 方法
  5. 读入Python脚本内容,通过exec函数动态执行该脚本

根据上面的流程分析一下,run_script方法肯定要修改以便加入多个用例;在每次用例执行前我们都需要修改工作目录,因此setUp也需要修改。既然修改点明确了,那么我们来梳理一下思路:

  1. run_script方法中先遍历目录获取出所有用例,然后统一添加都suite中
  2. setUp方法中调用auto_setup方法设置日志路径,实现结果分开存放
  3. 因为setUp方法中需要知道每个脚本的日志路径,所以要为AirtestCase添加一个属性保存日志路径并在run_script方法中为其设置对应的值

至此执行部分分析完成

Airtest 生成报告流程分析

Airtest 生成报告的核心代码位于airtest\report\report.py中,大体流程如下:

  1. 解析命令行参数
  2. 加载并解析 log.txt
  3. 使用jinja2渲染模板

因为我们的日志是分开存放的,要给每个日志都生成一个html报告只需在main方法里加个循环,遍历所有日志即可;生成聚合报告这里有点麻烦,因为需要获取LogToHtml内部的test_result属性,但是Python的动态特性允许我们使用types动态的为实例绑定方法,所以我们可以定义一个方法返回test_result属性,然后绑定给实例就可以了;最后自定义一个模板,把收集到的结果通过jinja2渲染进去,大功告成!

效果图

  • 项目目录结构
    项目目录结构

  • 生成的报告目录结构
    生成的报告目录结构

  • 聚合报告
    聚合报告

自行修改

核心的逻辑在 runner.py 中,可根据自己的实际情况进行修改,目前runner.py中的逻辑如下:

  1. 获取 runner.py 同目录下所有以“用例集”结尾的文件夹
  2. 遍历第一步获取的每个文件夹,找出其中的py文件(以.py结尾的文件,会忽略以“__”开头的缓存文件)添加到测试集
  3. 执行用例时会默认执行脚本中的runCase方法,所以需要将测试代码封装成runCase方法,签名如下runCase(self, vars)代码位置

欢迎大家多提宝贵意见,谢谢
就酱,上个链接,溜了溜了。。。(o゚v゚)ノ

我是链接

共收到 66 条回复 时间 点赞


请问一下这个是直接可以用的吗?为什么我把我的脚本放进去执行都是显示0个执行了

TD 回复

代码里加了过滤,文件夹必须以“用例集”结尾
代码链接

目录结构如下就能识别出来

case
├─report.py
├─runner.py
├─交易用例集
├──交易失败.py
└──交易成功.py
└─登录用例集
├──登录失败.py
└──登录成功.py

赞一个!回去试下

TD · #4 · October 24, 2018
Author only
TD 回复

报错是因为需要将测试代码封装成一个runCase方法,像下面那样

def runCase(self, vars):
# 业务逻辑代码
pass

代码在这里 new_case 方法

之所以这么整是考虑后面要实现数据驱动,可以通过方法签名里那个vars参数来传递数据,这样看起来比直接在命名空间里获取数据稍微清晰一点

TD · #6 · October 24, 2018
Author only
TD 回复

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)
xinufo 回复

你真是好人呀,谢谢啦👍 那报告生成是python report.py 日志的文件夹 吗?

TD 回复



我现在run之后生成日志了,但是生成报告的时候报错了,这是啥问题啊

参考你的方案做了一个新的方法出来,感谢大牛分享

xinufo #11 · October 25, 2018 作者
TD 回复

看信息应该是图片没找到,你看看生成日志文件夹里有对应的图片吗?正常情况文件夹里应该包含图片和log.txt

xinufo #12 · October 25, 2018 作者
cgt 回复

大牛不敢当,对你有帮助就好

TD · #13 · October 25, 2018
Author only
xinufo #14 · October 25, 2018 作者
TD 回复

方便上传下那个log_20181025_101740文件夹吗?看你截图分析不出来啥问题

xinufo 回复

附件可以上传的吗

xinufo #16 · October 25, 2018 作者
TD 回复

传到网盘里分享一下

TD · #17 · October 25, 2018
Author only
xinufo #18 · October 25, 2018 作者
TD 回复

那个_analyse方法我没改,直接调用的airtest框架原来的,要不你自己改下airtest/aircv/aircv.py(就是异常堆栈里的最后一个文件),把那个filename变量输出出来看看到底是什么值

TD · #19 · October 25, 2018
Author only
xinufo #20 · October 25, 2018 作者
TD 回复

我看你文件夹里确实没有那个文件啊,而且命令行里输出的图片是tpl开头的,你的日志文件夹里的图片都没有tpl前缀并且是jpg格式的。这一块在我写的report.py里我并没有做修改而是直接调用的原来的逻辑,我也不知道图片名称为啥出现这种差异😥

TD · #21 · October 26, 2018
Author only
xinufo #22 · October 26, 2018 作者
TD 回复

我刚拿poco试了一下是没问题,但是我用的是Python 3.6.5,不知道你那报错是不是因为你用Python 2.7的原因

xinufo 回复

好的,我再看看,谢谢啦

airtest 是什么? 好用么?我一直用selenium

xinufo #25 · October 28, 2018 作者

简单说就是基于图像识别的自动化框架,官网

大佬,想问一下和用命令行或者shell脚本批量运行有什么区别么?我现在是这么实现的
另外聚合报告这个挺好的,正缺这个😁

这个文件夹结构需要自己创建么,最好新建脚本的时候就能建好这个结构。。。这一步做了就比较方便了

Author only

请问下,该框架是否支持window应用的脚本呢?试过安卓APP脚本,是可以正常运行的,但执行windows应用时,报错了,报错信息如下:

xinufo #30 · November 16, 2018 作者
zlp 回复

文件夹结构是手动创建的,当时考虑脚本维护性就没按照官方那个air文件夹的结构

xinufo #31 · November 16, 2018 作者
CielLee 回复

需要手动调下结构,其实不调也能运行,需要把好多air的脚本和图片都放到一个目录里,这样很容易造成图片不知道是哪个脚本的,太乱;如果想要直接运行air脚本可以把runner.py里遍历文件的逻辑改下就行了

xinufo #32 · November 16, 2018 作者
sandy 回复

Windows的我没试过,但是案例来说应该是支持的,我只是修改了添加案例的逻辑,其他地方并没有做修改

Author only
xinufo #34 · November 19, 2018 作者

大神不敢当,当时写代码是确实是往这方面考虑的,但是最近比较忙可能抽不出时间来完善,如果你有好的想法或实现欢迎PR😀

Author only
TD 回复

你最后成功没呢?

Author only
38Floor has been deleted
xinufo #39 · January 15, 2019 作者
HazelRunner 回复

runner.py会自动查找跟它在同一目录下且名字以“用例集”结尾的文件夹,然后将找到的文件夹里的py文件加入都测试用例集中,你那找不到用例查一下是不是因为编码问题导致的

Author only




运行一直显示case为0,我是用的air作为后缀查找的,麻烦大神帮看下

请教下case内的py脚本有啥要求

-- encoding=utf8 --

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
我已经跑测试跑了一整天
有人知道为什么吗?

xinufo #44 · January 21, 2019 作者

需要封装成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()
45Floor has been deleted

你这个就只是修改了它原有的runner方法吧?

xinufo #47 · February 12, 2019 作者
Ki 回复

Airtest/tests/test_cli.py 这个也行,基于unittest

Author only
xinufo #50 · February 15, 2019 作者
simon 回复

不支持

51Floor has been deleted
52Floor has been deleted

rpt.report("log_template.html", output_file=output_file)
在执行上面一句时,报错

有人遇过这种情况吗



请问下,我已经封装成了runcase方法,但是执行报错,是什么原因

xinufo #55 · April 19, 2019 作者
lizhouquan 回复

报错是因为args找不到,这个是在run_script方法中定义的全局变量,直接把runner.py当主类启动

请问下大佬 , 在执行runner时报 NameError: name 'args' is not defined 的错

xinufo #57 · April 23, 2019 作者

报错是因为args找不到,这个是在run_script方法中定义的全局变量,直接把runner.py当主类启动.

借问高手

  1. poco有自带断言方法,如果不用poco的断言(exist, equl),而是用unittest或者pytest的断言,对报告会不会有影响,比如判断不出执行的失败与否

请问一下,为什么聚合后的报告没有图片?

horacexu 回复

我也遇到了,好像是因为 某个路径传错了 多了\ 但是不知道怎么改

柳锐神 回复

你的问题解决了吗?我也遇到了。。



日志下面有图片,格式也是正确的,但是在报告里面不显示截图,这个是什么情况呢?

bjxiehong 回复

首先我没用这里楼主的方法,而是百度的时候,发现其他大牛有不需要修改的源码的方法,大同小异。然后说这个问题,我调试半天发现路径里面 就是以\结尾,后来断点调试,发现是airtest的源码里面,对运行目录和脚本名字进行了拼接,而脚本名字又默认是空,导致拼接后就以\结尾了,这个路径系统是不认的,所以我尝试改了一下,直接不拼接了,,,没想到运行通过了……
具体就是 修改了源码report模块下面一行代码(不想修改源码,最后还是改了),具体截图如下

柳锐神 回复

嗯嗯,多谢大佬!

Rhett_ 回复

我的报告也没有图片,请问你的问题解决了吗?

sandy 回复

你解决了吗,我的windows也报错

xinufo #69 · June 11, 2019 作者
jiawei.li 回复

👍 👍 👍

bjxiehong 回复

你是在编译器打开的,编译器基于项目给一个ip,不显示图片是因为浏览器禁止访问本地资源!
你只需在文件中双击打开此文件就会正常显示了!!!

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up