通過(guò)本篇幔妨,你將了解到Airtest的自定義啟動(dòng)器的運(yùn)用,以及air腳本啟動(dòng)運(yùn)行的原理傍菇,還有批量執(zhí)行air腳本的方法猾瘸。
在用Airtest IDE可以編寫air腳本,運(yùn)行腳本,之后我們會(huì)想到那我怎么一次運(yùn)行多條腳本呢牵触?能不能用setup和teardown呢淮悼?答案是當(dāng)然可以,我們可以用自定義啟動(dòng)器揽思!參見(jiàn)官方文檔:7.3 腳本撰寫的高級(jí)特性
Airtest在運(yùn)行用例腳本時(shí)袜腥,在繼承unittest.TestCase的基礎(chǔ)上,實(shí)現(xiàn)了一個(gè)叫做AirtestCase的類钉汗,添加了所有執(zhí)行基礎(chǔ)Airtest腳本的相關(guān)功能羹令。因此,假如需要添加自定義功能损痰,只需要在AirtestCase類的基礎(chǔ)上福侈,往setup和teardown中加入自己的代碼即可。如果這些設(shè)置和功能內(nèi)容相對(duì)固定徐钠,可以將這些內(nèi)容作為一個(gè)launcher癌刽,用來(lái)在運(yùn)行實(shí)際測(cè)試用例之前初始化相關(guān)的自定義環(huán)境役首。
在這個(gè)自定義啟動(dòng)器里我們可以做什么呢尝丐?
添加自定義變量與方法
在正式腳本運(yùn)行前后,添加子腳本的運(yùn)行和其他自定義功能
修改Airtest默認(rèn)參數(shù)值
通過(guò)以下的例子看一下怎么實(shí)現(xiàn)衡奥,首先創(chuàng)建一個(gè)custom_launcher.py文件爹袁,實(shí)現(xiàn)以下代碼
from airtest.cli.runner import AirtestCase, run_script
from airtest.cli.parser import runner_parser
class CustomAirtestCase(AirtestCase):
PROJECT_ROOT = "子腳本存放公共路徑"
def setUp(self):
print("custom setup")
# add var/function/class/.. to globals
#將自定義變量添加到self.scope里,腳本代碼中就能夠直接使用這些變量
self.scope["hunter"] = "i am hunter"
self.scope["add"] = lambda x: x+1
#將默認(rèn)配置的圖像識(shí)別準(zhǔn)確率閾值改為了0.75
ST.THRESHOLD = 0.75
# exec setup script
# 假設(shè)該setup.air腳本存放在PROJECT_ROOT目錄下矮固,調(diào)用時(shí)無(wú)需填寫絕對(duì)路徑失息,可以直接寫相對(duì)路徑
self.exec_other_script("setup.air")
super(CustomAirtestCase, self).setUp()
def tearDown(self):
print("custom tearDown")
# exec teardown script
self.exec_other_script("teardown.air")
super(CustomAirtestCase, self).setUp()
if __name__ == '__main__':
ap = runner_parser()
args = ap.parse_args()
run_script(args, CustomAirtestCase)
然后,在IDE的設(shè)置中配置啟動(dòng)器
菜單-“選項(xiàng)”-“設(shè)置”-“Airtest”档址,點(diǎn)擊“自定義啟動(dòng)器”可打開文件選擇窗口盹兢,選擇自定義的launcher.py文件即可。
點(diǎn)擊“編輯”守伸,可對(duì)launcher.py文件的內(nèi)容進(jìn)行編輯绎秒,點(diǎn)擊“確定”按鈕讓新配置生效。
也可以用命令行啟動(dòng)
python custom_launcher.py test.air --device Android:///serial_num --log log_path
看到這里都沒(méi)有提供一次運(yùn)行多條腳本方法尼摹,但是有提供調(diào)用其他腳本的接口见芹,相信聰明的你應(yīng)該有些想法了,這個(gè)后面再講蠢涝,因?yàn)楣俜轿臋n里都說(shuō)了IDE確實(shí)沒(méi)有提供批量執(zhí)行腳本的功能呢
我們?cè)谀_本編寫完成后玄呛,AirtestIDE可以讓我們一次運(yùn)行單個(gè)腳本驗(yàn)證結(jié)果,但是假如我們需要在多臺(tái)手機(jī)上和二,同時(shí)運(yùn)行多個(gè)腳本徘铝,完成自動(dòng)化測(cè)試的批量執(zhí)行工作時(shí),AirtestIDE就無(wú)法滿足我們的需求了。目前可以通過(guò)命令行運(yùn)行手機(jī)的方式來(lái)實(shí)現(xiàn)批量多機(jī)運(yùn)行腳本惕它,例如在Windows系統(tǒng)中场晶,最簡(jiǎn)單的方式是直接編寫多個(gè)bat腳本來(lái)啟動(dòng)命令行運(yùn)行Airtest腳本。如果大家感興趣的話怠缸,也可以自行實(shí)現(xiàn)任務(wù)調(diào)度诗轻、多線程運(yùn)行的方案來(lái)運(yùn)行腳本。請(qǐng)注意揭北,若想同時(shí)運(yùn)行多個(gè)腳本扳炬,請(qǐng)盡量在本地Python環(huán)境下運(yùn)行,避免使用AirtestIDE來(lái)運(yùn)行腳本搔体。
劃重點(diǎn)恨樟!劃重點(diǎn)!劃重點(diǎn)疚俱!源碼分析來(lái)啦 劝术,以上都是“拾人牙慧”的搬運(yùn)教程,下面才是“精華”呆奕,我們開始看看源碼养晋。
從這個(gè)命令行啟動(dòng)的方式可以看出,這是用python運(yùn)行了custom_launcher.py文件梁钾,給傳入的參數(shù)是‘test.air’绳泉、‘device’、‘log’姆泻,那我們回去看一下custom_launcher.py的入口零酪。
if __name__ == '__main__':
ap = runner_parser()
args = ap.parse_args()
run_script(args, CustomAirtestCase)
runner_parser()接口是用ArgumentParser添加參數(shù)的定義
def runner_parser(ap=None):
if not ap:
ap = argparse.ArgumentParser()
ap.add_argument("script", help="air path")
ap.add_argument("--device", help="connect dev by uri string, e.g. Android:///", nargs="?", action="append")
ap.add_argument("--log", help="set log dir, default to be script dir", nargs="?", const=True)
ap.add_argument("--recording", help="record screen when running", nargs="?", const=True)
return ap
然后用argparse庫(kù)解析出命令行傳入的參數(shù)
# =====================================
# Command line argument parsing methods
# =====================================
def parse_args(self, args=None, namespace=None):
args, argv = self.parse_known_args(args, namespace)
if argv:
msg = _('unrecognized arguments: %s')
self.error(msg % ' '.join(argv))
return args
最后調(diào)用run_script(),把解析出來(lái)的args和我們實(shí)現(xiàn)的自定義啟動(dòng)器——CustomAirtestCase類一起傳進(jìn)去
def run_script(parsed_args, testcase_cls=AirtestCase):
global args # make it global deliberately to be used in AirtestCase & test scripts
args = parsed_args
suite = unittest.TestSuite()
suite.addTest(testcase_cls())
result = unittest.TextTestRunner(verbosity=0).run(suite)
if not result.wasSuccessful():
sys.exit(-1)
這幾行代碼拇勃,用過(guò)unittest的朋友應(yīng)該都很熟悉了四苇,傳入的參數(shù)賦值給一個(gè)全局變量以供AirtestCase和測(cè)試腳本調(diào)用,
1.創(chuàng)建一個(gè)unittest的測(cè)試套件方咆;
2.添加一條AirtestCase類型的case月腋,因?yàn)榻涌谌雲(yún)⒛J(rèn)testcase_cls=AirtestCase,也可以是CustomAirtestCase
3.用TextTestRunner運(yùn)行這個(gè)測(cè)試套件
所以Airtest的運(yùn)行方式是用的unittest框架峻呛,一個(gè)測(cè)試套件下只有一條testcase罗售,在這個(gè)testcase里執(zhí)行調(diào)用air腳本,具體怎么實(shí)現(xiàn)的繼續(xù)來(lái)看AirtestCase類钩述,這是CustomAirtestCase的父類寨躁,這部分代碼比較長(zhǎng),我就直接在源碼里寫注釋吧
class AirtestCase(unittest.TestCase):
PROJECT_ROOT = "."
SCRIPTEXT = ".air"
TPLEXT = ".png"
@classmethod
def setUpClass(cls):
#run_script傳進(jìn)來(lái)的參數(shù)轉(zhuǎn)成全局的args
cls.args = args
#根據(jù)傳入?yún)?shù)進(jìn)行初始化
setup_by_args(args)
# setup script exec scope
#所以在腳本中用exec_script就是調(diào)的exec_other_script接口
cls.scope = copy(globals())
cls.scope["exec_script"] = cls.exec_other_script
def setUp(self):
if self.args.log and self.args.recording:
#如果參數(shù)配置了log路徑且recording為Ture
for dev in G.DEVICE_LIST:
#遍歷全部設(shè)備
try:
#開始錄制
dev.start_recording()
except:
traceback.print_exc()
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):
#運(yùn)行腳本
#參數(shù)傳入的air腳本路徑
scriptpath = self.args.script
#根據(jù)air文件夾的路徑轉(zhuǎn)成py文件的路徑
pyfilename = os.path.basename(scriptpath).replace(self.SCRIPTEXT, ".py")
pyfilepath = os.path.join(scriptpath, pyfilename)
pyfilepath = os.path.abspath(pyfilepath)
self.scope["__file__"] = pyfilepath
#把py文件讀進(jìn)來(lái)
with open(pyfilepath, 'r', encoding="utf8") as f:
code = f.read()
pyfilepath = pyfilepath.encode(sys.getfilesystemencoding())
#用exec運(yùn)行讀進(jìn)來(lái)的py文件
try:
exec(compile(code.encode("utf-8"), pyfilepath, 'exec'), self.scope)
except Exception as err:
#出錯(cuò)處理牙勘,記錄日志
tb = traceback.format_exc()
log("Final Error", tb)
six.reraise(*sys.exc_info())
def exec_other_script(cls, scriptpath):
#這個(gè)接口不分析了职恳,因?yàn)橐呀?jīng)用using代替了所禀。
#這個(gè)接口就是在你的air腳本中如果用了exec_script就會(huì)調(diào)用這里,它會(huì)把子腳本的圖片文件拷過(guò)來(lái)放钦,并讀取py文件執(zhí)行exec
總結(jié)一下吧色徘,上層的air腳本不需要用到什么測(cè)試框架,直接就寫腳本操禀,是因?yàn)橛羞@個(gè)AirtestCase在支撐褂策,用runTest這一個(gè)測(cè)試用例去處理所有的air腳本運(yùn)行,這種設(shè)計(jì)思路確實(shí)降低了腳本的上手門檻颓屑,跟那些用excel表格和自然語(yǔ)言腳本的框架有點(diǎn)像斤寂。另外setup_by_args接口就是一些初始化的工作,如連接設(shè)備揪惦、日志等
#參數(shù)設(shè)置
def setup_by_args(args):
# init devices
if isinstance(args.device, list):
#如果傳入的設(shè)備參數(shù)是一個(gè)列表遍搞,所以命令行可以設(shè)置多個(gè)設(shè)備哦
devices = args.device
elif args.device:
#不是列表就給轉(zhuǎn)成列表
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
# 把a(bǔ)ir腳本的路徑設(shè)置為工程根目錄
project_root = os.path.dirname(args.script) if not ST.PROJECT_ROOT else None
# 設(shè)備的初始化連接,設(shè)置工程路徑器腋,日志路徑等溪猿。
auto_setup(args.script, devices, args.log, project_root)
好了,源碼分析就這么多纫塌,下面進(jìn)入實(shí)戰(zhàn)階段 诊县,怎么來(lái)做腳本的“批量運(yùn)行”呢?很簡(jiǎn)單护戳,有兩種思路:
用unittest框架翎冲,在testcase里用exec_other_script接口來(lái)調(diào)air腳本
自己寫一個(gè)循環(huán)垂睬,調(diào)用run_script接口媳荒,每次傳入不同的參數(shù)(不同air腳本路徑)
from launcher import Custom_luancher
from Method import Method
import unittest
from airtest.core.api import *
class TestCaseDemo(unittest.TestCase):
def setUp(self):
auto_setup(args.script, devices, args.log, project_root)
def test_01_register(self):
self.exec_other_script('test_01register.air')
def test_02_name(self):
self.exec_other_script('login.air')
self.exec_other_script('test_02add.air')
def tearDown(self):
Method.tearDown(self)
if __name__ == "__main__":
unittest.main()
def find_all_script(file_path):
'''查找air腳本'''
A = []
files = os.listdir(file_path)
for f1 in files:
tmp_path = os.path.join(file_path, files)
if not os.path.isdir(tmp_path):
pass
else:
if(tmp_path.endswith('.air')):
A.append(tmp_path)
else:
subList = find_all_script(tmp_path)
A = A+subList
return A
def run_airtest(path, dev=''):
'''運(yùn)行air腳本'''
log_path = os.path.join(path, 'log')
#組裝參數(shù)
args = Namespace(device=dev, log=log_path, recording=None, script=path)
try:
result = run_script(args, CustomLuancher)
except:
pass
finally:
if result and result.wasSuccessful():
return True
else:
return False
if __name__ == '__main__':
#查找指定路徑下的全部air腳本
air_list = find_all_script(CustomLuancher.PROJECT_ROOT)
for case in air_list:
result = run_airtest(case)
if not result:
print("test fail : "+ case)
else:
print("test pass : "+ case)
sys.exit(-1)
總結(jié),兩種方式實(shí)現(xiàn)Airtest腳本的批量執(zhí)行驹饺,各有優(yōu)缺點(diǎn)钳枕,自己體會(huì)吧,如果喜歡Airtest的結(jié)果報(bào)告建議用第二種方式赏壹,可以完整的保留日志鱼炒,結(jié)果以及啟動(dòng)運(yùn)行。第一種方式是自己寫的unittest來(lái)執(zhí)行蝌借,就沒(méi)有用的Airtest的啟動(dòng)器了昔瞧,報(bào)告部分要自己再處理一下,然后每添加一條air腳本菩佑,對(duì)應(yīng)這里也要加一條case自晰。