1. 程式人生 > >UI自動化測試工具AirTest學習筆記之自定義啟動器

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和測試指令碼呼叫,

  1. 建立一個unittest的測試套件;
  2. 新增一條AirtestCase型別的case,因為介面入參預設testcase_cls=AirtestCase,也可以是CustomAirtestCase
  3. 用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)

好了,原始碼分析就這麼多,下面進入實戰階段 ,怎麼來做指令碼的“批量執行”呢?很簡單,有兩種思路:

  1. 用unittest框架,在testcase裡用exec_other_script介面來調air指令碼
  2. 自己寫一個迴圈,呼叫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。