UI自動化測試工具AirTest學習筆記之自定義啟動器
通過本篇,你將瞭解到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。