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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章