用python做uiautomator測試
最近專案中有個需求要在至少100臺手機上對應用進行相容性測試,首先想到的就是自動化來操作,不想一臺臺的操作相同的重複操作
基本的需求是這樣的,安裝被測試的應用,啟動並退出,然後安裝測試樣本,檢測是否有相應的彈窗攔截
考慮到市面上的各種測試框架與自已熟悉的程式語言,最後選擇了google自家的uiautomator來搞,藉助於前人對其進行了python封裝,所以一開始還是挺順利的,但是整個過程中還是有很多需要注意的地方
準備:python27,不能使用python26,安裝urllib3與uiautomator,可以使用easy_install命令,安裝android SDK,配置好adb的環境變數,這些應該都是作為android測試人員最基本的環境配置,要測試的應用是360手機急救箱,可以從
下面是基本的測試流程
# 需要配置好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',第一次啟動會彈出使用協議要使用者來點選”同意並使用”
我這裡採用了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'允許')
點選同意後會再彈一個開啟超強模式的彈框,這裡我要點選的是取消
d.watcher('cancel').when(text=u'取消').click(text=u'取消')
之後要點選一下back鍵,這時又會彈一個是否退出的框,這次我要點選“確認”
這個確認我是後面單獨處理的,其實也可以放在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