UI自動(dòng)化測(cè)試工具AirTest學(xué)習(xí)筆記之自定義啟動(dòng)器

通過(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自晰。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市稍坯,隨后出現(xiàn)的幾起案子酬荞,更是在濱河造成了極大的恐慌搓劫,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件混巧,死亡現(xiàn)場(chǎng)離奇詭異枪向,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)咧党,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門秘蛔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人傍衡,你說(shuō)我怎么就攤上這事缠犀。” “怎么了聪舒?”我有些...
    開封第一講書人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵辨液,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我箱残,道長(zhǎng)滔迈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任被辑,我火速辦了婚禮燎悍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘盼理。我一直安慰自己谈山,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開白布宏怔。 她就那樣靜靜地躺著奏路,像睡著了一般。 火紅的嫁衣襯著肌膚如雪臊诊。 梳的紋絲不亂的頭發(fā)上鸽粉,一...
    開封第一講書人閱讀 49,071評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音抓艳,去河邊找鬼触机。 笑死,一個(gè)胖子當(dāng)著我的面吹牛玷或,可吹牛的內(nèi)容都是我干的儡首。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼偏友,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蔬胯!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起约谈,我...
    開封第一講書人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤笔宿,失蹤者是張志新(化名)和其女友劉穎犁钟,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泼橘,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涝动,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了炬灭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片醋粟。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖重归,靈堂內(nèi)的尸體忽然破棺而出米愿,到底是詐尸還是另有隱情,我是刑警寧澤鼻吮,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布育苟,位于F島的核電站,受9級(jí)特大地震影響椎木,放射性物質(zhì)發(fā)生泄漏违柏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一香椎、第九天 我趴在偏房一處隱蔽的房頂上張望漱竖。 院中可真熱鬧,春花似錦畜伐、人聲如沸馍惹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)万矾。三九已至,卻和暖如春脚仔,著一層夾襖步出監(jiān)牢的瞬間勤众,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工鲤脏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吕朵。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓猎醇,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親努溃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子硫嘶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容