1. 程式人生 > >用python做uiautomator測試

用python做uiautomator測試

最近專案中有個需求要在至少100臺手機上對應用進行相容性測試,首先想到的就是自動化來操作,不想一臺臺的操作相同的重複操作

基本的需求是這樣的,安裝被測試的應用,啟動並退出,然後安裝測試樣本,檢測是否有相應的彈窗攔截

考慮到市面上的各種測試框架與自已熟悉的程式語言,最後選擇了google自家的uiautomator來搞,藉助於前人對其進行了python封裝,所以一開始還是挺順利的,但是整個過程中還是有很多需要注意的地方

準備:python27,不能使用python26,安裝urllib3與uiautomator,可以使用easy_install命令,安裝android SDK,配置好adb的環境變數,這些應該都是作為android測試人員最基本的環境配置,要測試的應用是360手機急救箱,可以從

http://jijiu.360.cn/ 這個網址下載

下面是基本的測試流程

# 需要配置好adb 環境變數 
# 1.先確定有幾臺手機 
# 2.再確定有多少個應用 
# 3.先安裝mkiller,啟動mkiller 
# 4.再安裝測試的樣本 
# 5.檢查是否有取消安裝的按鈕出現,出現說明測試通過,沒出現說明測試失敗

既然要採用自動化,就不能手機測試那樣,一臺一臺的跑,應該可以同時跑多臺手機,我的想法就是啟用多執行緒來跑,每個手機用一個執行緒來跑

確定有幾臺手機,我封了一個方法

deffinddevices():
    rst = util.exccmd('adb devices')
    devices = re.findall(r'(.*?)\s+device'
,rst) if len(devices) > 1: deviceIds = devices[1:] logger.info('共找到%s個手機'%str(len(devices)-1)) for i in deviceIds: logger.info('ID為%s'%i) return deviceIds else: logger.error('沒有找到手機,請檢查') return

下面來說說uiautomator在python中的使用,其實github中的readme.md寫的挺清楚,但是實踐起來還是有一些問題

uiautomator在使用的時候都要初始化一個d物件,單個手機可以通過

from uiautomator import device as d

多臺手機可以

from uiautomator import Device

然後通過 d=Device(Serial)的方式初始化d物件,以後的操作基本上都是操作這個d物件,可以想象每個d對應著一臺手機

我覺得這個設計有點不大好,我現在還經常在device的大小寫上犯迷糊

基本的點選操作

# press home key
d.press.home()
# press back key
d.press.back()
# the normal way to press back key
d.press("back")
# press keycode 0x07('0') with META ALT(0x02) on
d.press(0x07, 0x02)

首先安裝啟動應用,安裝採用adb install 命令,啟動採用adb shell am start 命令

手機急救箱的launchable-activity是'com.qihoo.mkiller.ui.index.AppEnterActivity',第一次啟動會彈出使用協議要使用者來點選”同意並使用”

dump_3164510212585030182

我這裡採用了watcher來監視並且點選,基本的watcher方法是

d.watcher('agree').when(text=u'同意並使用').click(text=u'同意並使用')

先給watcher起一個名字,隨便起,我這裡叫agree,when裡面寫條件,我這裡就是當text為’同意並使用’,後面寫當符合這些條件的時候進行的操作,我這裡就是click(text=u'同意並使用'),這裡有一個坑,我之前寫watcher的時候,就直接寫click() 我以為裡面不寫內容預設就會點選前面找到的元素,但是後來發現這樣是不行的,必須要寫上要點選哪個物件

其實對於這種只出現一次的view可以不用寫在watcher裡,可以直接寫d(text=u'同意並使用').click(),但是考慮到這個介面出現之前會有一些延遲,各種手機的效能不同,也不好加time.sleep()時間,所以我建議像這種一律寫到watcher裡,什麼時候出現就什麼時候點選。

由於這個應用會請求root許可權,所以有時第三方的root工具會彈相應的授權提示框,我想大部分的root工具應該都是有”允許”這個按鈕的,於是我就加了一個watcher

d.watcher('allowroot').when(text=u'允許').click(text=u'允許')

點選同意後會再彈一個開啟超強模式的彈框,這裡我要點選的是取消

dump_3893862848196469772

d.watcher('cancel').when(text=u'取消').click(text=u'取消')

之後要點選一下back鍵,這時又會彈一個是否退出的框,這次我要點選“確認”

dump_8250787869838979823

這個確認我是後面單獨處理的,其實也可以放在watcher裡,只是我的考慮是有時點選back鍵的時候不一定會彈出來這個框,所以我會嘗試多點選幾次,直到這個框出來

但現在就有一個問題了,剛才寫了一個d.watcher('cancel').when(text=u'取消').click(text=u'取消'),這時當彈出這個框的時候,watcher就要起作用了,就會先去點選取消,這不是我想要的,所以我將之前點選取消的加了一個限制條件

d.watcher('cancel').when(text=u'取消').when(textContains=u'超強防護能夠極大提高').click(text=u'取消')

textContains的意思就是和包含裡面的文字,上面的意思就是當介面中text是“取消”的同時還要有一個view的text中要包含u'超強防護能夠極大提高',這樣的話就限制的點選“取消”的條件,再遇到退出時的提示框就不會再會點選”取消”了

儘可能的想到可能出現的彈框,比較在小米手機中安裝應用會彈一個小米的安裝確認介面,使用下面的watcher來進行監測點選

d.watcher('install').when(text=u'安裝').when(textContains=u'是否要安裝該應用程式').click(text=u'安裝',className='android.widget.Button')

總的watcher就是下面的樣子

d.watcher('allowroot').when(text=u'允許').click(text=u'允許')
        d.watcher('install').when(text=u'安裝').when(textContains=u'是否要安裝該應用程式').click(text=u'安裝',className='android.widget.Button') #專門為小米彈出的安裝攔截
        d.watcher('cancel').when(text=u'取消').when(textContains=u'超強防護能夠極大提高').click(text=u'取消')
        d.watcher('confirm').when(text=u'確認').when(textContains=u'應用程式許可').click(text=u'確認')
        d.watcher('agree').when(text=u'同意並使用').click(text=u'同意並使用')
        d.watcher('weishiuninstall').when(textContains=u'暫不處理').click(textContains=u'暫不處理')

然後使用d.watchers.run()來啟動watcher

但是在實際的watcher中,我發現這個watcher並沒有想象的那樣好用,有時經常是明明有相應的view但是就是點選不上,經過多次嘗試,我發現,當介面已經出現的時候,這時我再強行的使用run()方法來啟動watchers,這時它就能很好的點選了,所以基於此,我寫了一個迴圈來來無限的呼叫run方法,times限制了次數,根據專案的實際進行調整吧,sleep時間也可以相應的調整

defrunwatch(d,data):
    times = 120
    while True:
        if data == 1:                
            return True
        # d.watchers.reset()
        d.watchers.run()        
        times -= 1
        if times == 0:
            break
        else:
            time.sleep(0.5)

監視的時候又不能只跑監視程式,還要跑相應的測試步驟,所以這裡我把這個runwatch方法放到一個執行緒中去跑,起一個執行緒用作監視,指令碼的測試方法放在另外的執行緒上跑

執行緒函式

#執行緒函式
classFuncThread(threading.Thread):
    def__init__(self, func, *params, **paramMap):
        threading.Thread.__init__(self)
        self.func = func
        self.params = params
        self.paramMap = paramMap
        self.rst = None
        self.finished = False

    defrun(self):
        self.rst = self.func(*self.params, **self.paramMap)
        self.finished = True

    defgetResult(self):
        return self.rst

    defisFinished(self):
        return self.finished

defdoInThread(func, *params, **paramMap):
    t_setDaemon = None
    if 't_setDaemon' in paramMap:
        t_setDaemon = paramMap['t_setDaemon']
        del paramMap['t_setDaemon']
    ft = FuncThread(func, *params, **paramMap)
    if t_setDaemon != None:
        ft.setDaemon(t_setDaemon)
    ft.start()
    return ft

所以這裡啟動執行緒來跑runwatcher的呼叫就是

data = 0

doInThread(runwatch,d,data,t_setDaemon=True)

基本的思路就是這樣,這樣當指令碼都寫完了以後在單個手機上執行很好,但是一旦插入多個手機就會出現一個問題,所有watcher只在一臺手機上有效,另外的手機就只能傻傻的不知道點選,這個問題困擾了很久,我在github上也給作者發issue,但是後來我自已找到了解決的辦法,就是在d=Device(Serial)的時候加上local_port埠號,讓每臺手機使用不同的local_port埠號,這樣各自執行各自的,都很完好

以下了測試指令碼的程式碼

mkiller.py,主測試指令碼檔案

#coding:gbk

import os,sys,time,re,csv
import log
import util
from uiautomator import Device
import traceback
import log,logging
import multiprocessing



optpath = os.getcwd()                      #獲取當前操作目錄
imgpath = os.path.join(optpath,'img')      #截圖目錄

defcleanEnv():
    os.system('adb kill-server')
    needClean = ['log.log','img','rst']
    pwd = os.getcwd()
    for i in needClean:
        delpath = os.path.join(pwd,i)
        if os.path.isfile(delpath):
            cmd = 'del /f/s/q "%s"'% delpath
            os.system(cmd)
        elif os.path.isdir(delpath):
            cmd = 'rd /s/q "%s"' %delpath
            os.system(cmd)
    if not os.path.isdir('rst'):
        os.mkdir('rst')
        
defrunwatch(d,data):
    times = 120
    while True:
        if data == 1:                
            return True
        # d.watchers.reset()
        d.watchers.run()        
        times -= 1
        if times == 0:
            break
        else:
            time.sleep(0.5)
            
definstallapk(apklist,d,device):
    sucapp = []
    errapp = []
    # d = Device(device)
    #初始化一個結果檔案
    d.screen.on()
    rstlogger = log.Logger('rst/%s.log'%device,clevel = logging.DEBUG,Flevel = logging.INFO)
    #先安裝mkiller
    mkillerpath = os.path.join(os.getcwd(),'MKiller_1001.apk')
    cmd = 'adb -s %s install -r %s'% (device,mkillerpath)
    util.exccmd(cmd)
    defcheckcancel(d,sucapp,errapp):
        times = 10
        while(times):
            if d(textContains = u'取消安裝').count:
                print d(textContains = u'取消安裝',className='android.widget.Button').info['text']
                d(textContains = u'取消安裝',className='android.widget.Button').click()
                rstlogger.info(device+'測試成功,有彈出取消安裝對話方塊')
                break
            else:
                time.sleep(1)
                times -= 1
                if times == 0:
                    rstlogger.error(device+'測試失敗,沒有彈出取消安裝對話方塊')
    try:        
        d.watcher('allowroot').when(text=u'允許').click(text=u'允許')
        d.watcher('install').when(text=u'安裝').when(textContains=u'是否要安裝該應用程式').click(text=u'安裝',className='android.widget.Button') #專門為小米彈出的安裝攔截
        d.watcher('cancel').when(text=u'取消').when(textContains=u'超強防護能夠極大提高').click(text=u'取消')
        d.watcher('confirm').when(text=u'確認').when(textContains=u'應用程式許可').click(text=u'確認')
        d.watcher('agree').when(text=u'同意並使用').click(text=u'同意並使用')
        d.watcher('weishiuninstall').when(textContains=u'暫不處理').click(textContains=u'暫不處理')
        # d.watchers.run()
        data = 0
        util.doInThread(runwatch,d,data,t_setDaemon=True)
        #啟動急救箱並退出急救箱
        cmd = 'adb -s %s shell am start com.qihoo.mkiller/com.qihoo.mkiller.ui.index.AppEnterActivity'% device
        util.exccmd(cmd)
        time.sleep(5)
        times = 3
        while(times):
            d.press.back()
            if d(text=u'確認').count:
                d(text=u'確認').click()
                break
            else:
                time.sleep(1)
                times -=1
                
        for item in apklist:
            apkpath = item
            if not os.path.exists(apkpath):
                logger.error('%s的應用不存在,請檢查'%apkpath)
                continue 
            if not device:
                cmd = 'adb install -r "%s"' % apkpath
            else:
                cmd = 'adb -s %s install -r "%s"'%(device,apkpath)
            util.doInThread(checkcancel,d,sucapp,errapp)
            rst = util.exccmd(cmd)
    except Exception, e:
        logger.error(traceback.format_exc())
        data = 1
    data = 1
    return sucapp
            
deffinddevices():
    rst = util.exccmd('adb devices')
    devices = re.findall(r'(.*?)\s+device',rst)
    if len(devices) > 1:
        deviceIds = devices[1:]
        logger.info('共找到%s個手機'%str(len(devices)-1))
        for i in deviceIds:            
            logger.info('ID為%s'%i)
        return deviceIds
    else:
        logger.error('沒有找到手機,請檢查')
        return 
        
#needcount:需要安裝的apk數量,預設為0,既安所有
#deviceids:手機的列表
#apklist:apk應用程式的列表
defdoInstall(deviceids,apklist):
    count = len(deviceids)
    port_list = range(5555,5555+count)
    for i in range(len(deviceids)):
        d = Device(deviceids[i],port_list[i])
        util.doInThread(installapk,apklist,d,deviceids[i])
       
#結束應用
defuninstall(deviceid,packname,timeout=20):
    cmd = 'adb -s %s uninstall %s' %(deviceid,packname) 
    ft = util.doInThread(os.system,cmd,t_setDaemon=True)
    while True:
        if ft.isFinished():
            return True
        else:
            time.sleep(1)
            timeout -= 1
            if timeout == 0:
                return False
         

# 需要配置好adb 環境變數
# 1.先確定有幾臺手機
# 2.再確定有多少個應用
# 3.先安裝mkiller,啟動mkiller
# 4.再安裝測試的樣本
# 5.檢查是否有取消安裝的按鈕出現,出現說明測試通過,沒出現說明測試失敗


if __name__ == "__main__":
    cleanEnv()
    logger = util.logger
    devicelist = finddevices()
    if devicelist:       
        apkpath = os.path.join(os.getcwd(),'apk')
        apklist = util.listFile(apkpath)
        doInstall(devicelist,apklist) #每個手機都要安裝apklist裡的apk

util.py 執行緒與執行cmd指令碼函式檔案

#coding:gbk
import os,sys
import log
import logging
import threading
import multiprocessing
import time

logger = log.Logger('log.log',clevel = logging.DEBUG,Flevel = logging.INFO)

defexccmd(cmd):
    try:
        return os.popen(cmd).read()
    except Exception:
        return None
    
#遍歷目錄內的檔案列表
deflistFile(path, isDeep=True):
    _list = []
    if isDeep:
        try:
            for root, dirs, files in os.walk(path):
                for fl in files:
                    _list.append('%s\%s' % (root, fl))
        except:
            pass
    else:
        for fn in glob.glob( path + os.sep + '*' ):
            if not os.path.isdir(fn):
                _list.append('%s' % path + os.sep + fn[fn.rfind('\\')+1:])
    return _list
    
#執行緒函式
classFuncThread(threading.Thread):
    def__init__(self, func, *params, **paramMap):
        threading.Thread.__init__(self)
        self.func = func
        self.params = params
        self.paramMap = paramMap
        self.rst = None
        self.finished = False

    defrun(self):
        self.rst = self.func(*self.params, **self.paramMap)
        self.finished = True

    defgetResult(self):
        return self.rst

    defisFinished(self):
        return self.finished

defdoInThread(func, *params, **paramMap):
    t_setDaemon = None
    if 't_setDaemon' in paramMap:
        t_set