通過本篇,你將瞭解到Airtest的自定義啓動器的運用,以及air腳本啓動運行的原理,還有批量執行air腳本的方法。
在用Airtest IDE可以編寫air腳本,運行腳本,之後我們會想到那我怎麼一次運行多條腳本呢?能不能用setup和teardown呢?答案是當然可以,我們可以用自定義啓動器!參見官方文檔:7.3 腳本撰寫的高級特性
Airtest在運行用例腳本時,在繼承unittest.TestCase的基礎上,實現了一個叫做AirtestCase的類,添加了所有執行基礎Airtest腳本的相關功能。因此,假如需要添加自定義功能,只需要在AirtestCase類的基礎上,往setup和teardown中加入自己的代碼即可。如果這些設置和功能內容相對固定,可以將這些內容作爲一個launcher,用來在運行實際測試用例之前初始化相關的自定義環境。
在這個自定義啓動器裏我們可以做什麼呢?
- 添加自定義變量與方法
- 在正式腳本運行前後,添加子腳本的運行和其他自定義功能
- 修改Airtest默認參數值
通過以下的例子看一下怎麼實現,首先創建一個custom_launcher.py文件,實現以下代碼
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
#將默認配置的圖像識別準確率閾值改爲了0.75
ST.THRESHOLD = 0.75
# exec setup script
# 假設該setup.air腳本存放在PROJECT_ROOT目錄下,調用時無需填寫絕對路徑,可以直接寫相對路徑
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的設置中配置啓動器
菜單-“選項”-“設置”-“Airtest”,點擊“自定義啓動器”可打開文件選擇窗口,選擇自定義的launcher.py文件即可。
點擊“編輯”,可對launcher.py文件的內容進行編輯,點擊“確定”按鈕讓新配置生效。
也可以用命令行啓動
python custom_launcher.py test.air --device Android:///serial_num --log log_path
看到這裏都沒有提供一次運行多條腳本方法,但是有提供調用其他腳本的接口,相信聰明的你應該有些想法了,這個後面再講,因爲官方文檔裏都說了IDE確實沒有提供批量執行腳本的功能呢
我們在腳本編寫完成後,AirtestIDE可以讓我們一次運行單個腳本驗證結果,但是假如我們需要在多臺手機上,同時運行多個腳本,完成自動化測試的批量執行工作時,AirtestIDE就無法滿足我們的需求了。
目前可以通過命令行運行手機的方式來實現批量多機運行腳本,例如在Windows系統中,最簡單的方式是直接編寫多個bat腳本來啓動命令行運行
Airtest
腳本。如果大家感興趣的話,也可以自行實現任務調度、多線程運行的方案來運行腳本。請注意,若想同時運行多個腳本,請儘量在本地Python環境下運行,避免使用AirtestIDE來運行腳本。
劃重點!劃重點!劃重點!源碼分析來啦 ,以上都是“拾人牙慧”的搬運教程,下面纔是“精華”,我們開始看看源碼。
從這個命令行啓動的方式可以看出,這是用python運行了custom_launcher.py文件,給傳入的參數是‘test.air’、‘device’、‘log’,那我們回去看一下custom_launcher.py的入口。
if __name__ == '__main__':
ap = runner_parser()
args = ap.parse_args()
run_script(args, CustomAirtestCase)
runner_parser()接口是用ArgumentParser添加參數的定義
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庫解析出命令行傳入的參數
# =====================================
# 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
最後調用run_script(),把解析出來的args和我們實現的自定義啓動器——CustomAirtestCase類一起傳進去
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)
這幾行代碼,用過unittest的朋友應該都很熟悉了,傳入的參數賦值給一個全局變量以供AirtestCase和測試腳本調用,
- 創建一個unittest的測試套件;
- 添加一條AirtestCase類型的case,因爲接口入參默認testcase_cls=AirtestCase,也可以是CustomAirtestCase
- 用TextTestRunner運行這個測試套件
所以Airtest的運行方式是用的unittest框架,一個測試套件下只有一條testcase,在這個testcase裏執行調用air腳本,具體怎麼實現的繼續來看AirtestCase類,這是CustomAirtestCase的父類,這部分代碼比較長,我就直接在源碼裏寫註釋吧
class AirtestCase(unittest.TestCase):
PROJECT_ROOT = "."
SCRIPTEXT = ".air"
TPLEXT = ".png"
@classmethod
def setUpClass(cls):
#run_script傳進來的參數轉成全局的args
cls.args = args
#根據傳入參數進行初始化
setup_by_args(args)
# setup script exec scope
#所以在腳本中用exec_script就是調的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:
#如果參數配置了log路徑且recording爲Ture
for dev in G.DEVICE_LIST:
#遍歷全部設備
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):
#運行腳本
#參數傳入的air腳本路徑
scriptpath = self.args.script
#根據air文件夾的路徑轉成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文件讀進來
with open(pyfilepath, 'r', encoding="utf8") as f:
code = f.read()
pyfilepath = pyfilepath.encode(sys.getfilesystemencoding())
#用exec運行讀進來的py文件
try:
exec(compile(code.encode("utf-8"), pyfilepath, 'exec'), self.scope)
except Exception as err:
#出錯處理,記錄日誌
tb = traceback.format_exc()
log("Final Error", tb)
six.reraise(*sys.exc_info())
def exec_other_script(cls, scriptpath):
#這個接口不分析了,因爲已經用using代替了。
#這個接口就是在你的air腳本中如果用了exec_script就會調用這裏,它會把子腳本的圖片文件拷過來,並讀取py文件執行exec
總結一下吧,上層的air腳本不需要用到什麼測試框架,直接就寫腳本,是因爲有這個AirtestCase在支撐,用runTest這一個測試用例去處理所有的air腳本運行,這種設計思路確實降低了腳本的上手門檻,跟那些用excel表格和自然語言腳本的框架有點像。另外setup_by_args接口就是一些初始化的工作,如連接設備、日誌等
#參數設置
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
# 把air腳本的路徑設置爲工程根目錄
project_root = os.path.dirname(args.script) if not ST.PROJECT_ROOT else None
# 設備的初始化連接,設置工程路徑,日誌路徑等。
auto_setup(args.script, devices, args.log, project_root)
好了,源碼分析就這麼多,下面進入實戰階段 ,怎麼來做腳本的“批量運行”呢?很簡單,有兩種思路:
- 用unittest框架,在testcase裏用exec_other_script接口來調air腳本
- 自己寫一個循環,調用run_script接口,每次傳入不同的參數(不同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=''):
'''運行air腳本'''
log_path = os.path.join(path, 'log')
#組裝參數
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)
總結,兩種方式實現Airtest腳本的批量執行,各有優缺點,自己體會吧,如果喜歡Airtest的結果報告建議用第二種方式,可以完整的保留日誌,結果以及啓動運行。第一種方式是自己寫的unittest來執行,就沒有用的Airtest的啓動器了,報告部分要自己再處理一下,然後每添加一條air腳本,對應這裏也要加一條case。