There is a requirement in the recent project to test the application on at least 100 phones, the first thing to think about is the automation to operate, do not want to operate the same duplicate operation
The basic requirement is this, install the tested application, start and exit, and then install the test sample to detect if there is a corresponding pop-up window blocker
Considering the various testing frameworks on the market and the familiar programming language, finally chose Google's own uiautomator to do, with the help of the previous Python package, so it was quite smooth at first, but there are a lot of things to notice in the process
Https://github.com/xiaocong/uiautomator This is the Python package that Xiaocong is using for this small test case, the cult
Preparation: Python27, cannot use Python26, install URLLIB3 and uiautomator, can use Easy_install command, install Android SDK, configure the environment variables of ADB, These should be the most basic environment configuration for Android testers, the application to be tested is 360 mobile phone first aid box, can be downloaded from http://jijiu.360.cn/this website
Here's the basic test process
# The ADB environment variables need to be configured.
# 1. Make sure you have several phones first
# 2. Determine how many applications
# 3. Install Mkiller First, start Mkiller
# 4. Re-install the test sample
# 5. Check if there is a Cancel Install button appears, the description test passed, did not show that the test failed
Since the use of automation, it can not be a mobile phone test, a single run, should be able to run multiple mobile phones at the same time, my idea is to enable multi-threading to run, each cell phone with a thread to run
Make sure there are several phones, and I've blocked a way
Def finddevices (): rst = util.exccmd (' adb devices ') devices = Re.findall (R ' (. *?) \s+device ', rst) if Len (Devices) > 1: deviceids = devices[1:] logger.info (' Find%s phones '%str (len (Devices) -1)) for i in DeviceIDs: logger.info (' ID is%s '%i) return deviceids else: logger.error (' No phone found, Please check ') return
The following is the use of uiautomator in Python, in fact, GitHub's readme.md writing is quite clear, but there are still some problems in practice.
Uiautomator Initializes a D object when used, and a single phone can be
From Uiautomator import device as D
More than one phone can
From Uiautomator import Device
The D object is then initialized by D=device (Serial), which is basically manipulating the D object, and you can imagine that each d corresponds to a cell phone
I think this is a little bit of a design, and I'm often confused about the size of the device.
Basic click-to-action
# Press Home Keyd.press.home () # Press Back Keyd.press.back () # The Normal-keyd.press ("Back") # press Keyco De 0x07 (' 0 ') with META ALT (0x02) ond.press (0x07, 0x02)
First install the launch application, install the ADB command, start the ADB shell am start command
Phone First Aid box launchable-activity is ' com.qihoo.mkiller.ui.index.AppEnterActivity ', the first launch will pop up using the protocol to the user to click on "Agree and use"
I use Watcher to monitor and click here, the Basic Watcher method is
D.watcher (' agree '). When (Text=u ' agrees and uses '). Click (Text=u ' agree and use ')
First give Watcher a name, casually, I here called agree,when inside write condition, I here is when text for ' agree and use ', behind write when meet these conditions of operation, I here is click (text=u ' agree and use '), here is a pit, When I wrote Watcher, I wrote the click directly () I thought the content was not written in the default will click on the previous found element, but later found that this is not possible, you must write on which object to click
In fact, for this one-time view can not be written in the Watcher, you can directly write D (text=u ' consent and use '). Click (), but considering that there will be some delay before the interface appears, the performance of various phones is different, also not good add time.sleep () time, So I suggest that it be written in watcher, and when it appears, click.
Since this app will request root permissions, sometimes the root tool of a third party will play the corresponding authorization prompt, I think most of the root tools should have "allow" this button, so I added a watcher
D.watcher (' Allowroot '). When (Text=u ' Allow '). Click (Text=u ' Allow ')
Click on the consent will play another open super mode of the box, here I want to click on the Cancel
D.watcher (' Cancel '). When (Text=u ' Cancel '). Click (Text=u ' Cancel ')
After you click the Back button, then will play a box to quit, this time I want to click "Confirm"
This confirmation I was handled separately, in fact, can also be placed in the watcher, just my consideration is sometimes clicked back key when not necessarily will pop up to this box, so I will try to click a few times, until this box out
But now there is a problem, just wrote a D.watcher (' Cancel '). When (Text=u ' Cancel '). Click (Text=u ' Cancel '), then when the box pops up, the watcher is going to work, it will first click Cancel, This is not what I want, so I'll add a limit to the previous click Cancellation
D.watcher (' Cancel '). When (Text=u ' cancellation '). When (Textcontains=u ' super protection can be greatly improved '). Click (Text=u ' Cancel ')
Textcontains means and contains the text, the above means that when the text in the interface is "canceled" while also have a view of the text to include U ' super protection can greatly improve, so that the limit of click "Cancel" condition, You will not be able to click "Cancel" again when you encounter the exit notification box.
As much as possible to think of the possible appearance of the box, compared to the Xiaomi phone installed application will play a Xiaomi installation confirmation interface, using the following watcher to monitor the click
D.watcher (' Install '). When (text=u ' installation '). When (Textcontains=u ' to install the application '). Click (text=u ' Install ', classname= ' Android.widget.Button ')
The total watcher is like this.
D.watcher (' Allowroot '). When (Text=u ' Allow '). Click (Text=u ' Allow ') d.watcher (' Install '). When ( text=u ' installation '). When ( Textcontains=u ' Whether to install the application '). Click (text=u ' Install ', classname= ' Android.widget.Button ') #专门为小米弹出的安装拦截 d.watcher (' Cancel '). When (Text=u ' cancellation '). When (Textcontains=u ' ultra-strong protection can be greatly improved '). Click (Text=u ' Cancel ') d.watcher (' confirm '). When ( Text=u ' confirmation '). When (textcontains=u ' Application License '). Click (text=u ' confirm ') D.watcher (' agree '). When (Text=u ' agree and use '). Click ( Text=u ' agree and use ') d.watcher (' Weishiuninstall '). When (Textcontains=u ' temporarily not processed '). Click (Textcontains=u ' temporarily not processed ')
Then use D.watchers.run () to start the Watcher
But in the actual watcher, I found that this watcher is not as good as it is imagined, sometimes it is obvious that there is a corresponding view but not the click, after many attempts, I found that when the interface has appeared, then I forcibly use the run () method to start watchers, then it can be very good click, so based on this, I wrote a loop to unlimited call the Run method, times limit the number, according to the actual project to adjust it, sleep time can be adjusted accordingly
def runwatch (D,data): Times = true: if data = = 1: return True # d.watchers.reset () D.watchers.run () times- = 1 if times = = 0: Break Else: time.sleep (0.5)
Monitoring the time can not only run the monitoring program, but also to run the corresponding test steps, so here I put this runwatch method in a thread to run, a thread as a monitor, script test method on another thread to run
Thread functions
#线程函数class Functhread (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 def run (self): Self.rst = Self.func (*self.params, **self.parammap) se lf.finished = True def getresult (self): return Self.rst def isfinished (self): return self.finisheddef Dointhread (func, *params, **parammap): T_setdaemon = None if ' T_setdaemon ' in Parammap:t_setdaemon = Paramma p[' T_setdaemon ' del parammap[' T_setdaemon '] ft = Functhread (func, *params, **parammap) if t_setdaemon! = Non E:ft.setdaemon (T_setdaemon) Ft.start () return ft
So this is where the thread runs and the Runwatcher call is
data = 0
Dointhread (Runwatch,d,data,t_setdaemon=true)
The basic idea is this, so that when the script is finished in a single phone to run very well, but once the insertion of multiple phones there will be a problem, all watcher only on a mobile phone, the other phone can only be silly do not know the click, this problem plagued a long time, I also give the author issue on GitHub, but later I have found a solution, that is, in D=device (Serial) with the Local_port port number, let each phone use a different local_port port number, so each run their own, All in good condition.
The following code for the test script
mkiller.py, master Test script file
#coding: Gbkimport os,sys,time,re,csvimport logimport utilfrom uiautomator import deviceimport tracebackimport log, Loggingimport Multiprocessingoptpath = OS.GETCWD () #获取当前操作目录imgpath = Os.path.join (Optpath, ' img ') #目录def cleanenv (): 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 ') def runwatch (d,data): Times = + True:if data = = 1:return True # d.watchers.reset () D.watchers.run () Times-= 1 if times = = 0:break Else:time.sleep (0.5) def Insta LLAPK (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) def checkcancel (D,sucapp,errapp): Times = 10 while (times): if d (textcontains = U ' Cancel installation '). Count:print d (textcontains = U ' Cancel installation ', Classname= ' and Roid.widget.Button '). info[' text '] D (textcontains = U ' Cancel installation ', Classname= ' Android.widget.Button '). Click () Rstlogger.info (device+ ' test succeeded, there was a popup cancel Installation dialog box ') Break Else:time.sleep (1) Times-= 1 if times = = 0:rstlogger.error (device+ ' test failed, no popup Cancel Installation dialog ') Try: D.watcher (' Allowroot '). When (Text=u ' Allow '). Click (Text=u ' Allow ') d.watcher (' Install '). When (text=u ' installation '). When (TextContains=u ' Whether to install the application '). Click (text=u ' Install ', classname= ' Android.widget.Button ') #专门为小米弹出的安装拦截 d.watcher (' Cancel '). When (text=u ' Cancel '). When (Textcontains=u ' extra-strong protection can be greatly improved '). Click (Text=u ' Cancel ') d.watcher (' Confirm '). When (text=u ' confirm '). When ( Textcontains=u ' Application License '). Click (text=u ' Confirm ') D.watcher (' agree '). When (Text=u ' agree and use '). Click (Text=u ' agree and use ') D.WATC She (' Weishiuninstall '). When (Textcontains=u ' temporarily not processed '). Click (Textcontains=u ' temporarily not processing ') # D.watchers.run () data = 0 Util.dointhread (runwatch,d,data,t_setdaemon=true) #启动急救箱并退出急救箱 cmd = ' adb-s%s shell am start Com.qiho O.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 ' ack '). Count:d (text=u ' ack '). Click () Break Else:time.sleep (1) Times-=1 for ITE M in Apklist:apkpath= Item if not os.path.exists (Apkpath): Logger.error ('%s ' app does not exist, please check '%apkpath ') Conti Nue 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 suc App Def finddevices (): rst = Util.exccmd (' adb devices ') devices = Re.findall (R ' (. *?) \s+device ', rst) if Len (Devices) > 1:deviceids = devices[1:] Logger.info (' Find%s phones '%str (len (Devices)-1 ))) for I in DeviceIds:logger.info (' ID is%s '%i) return deviceids Else:logger . Error (' No phone found, please check ') return #needcount: Number of apk to install, default to 0, all #deviceids: List of Phones #apklist:apk application list def doinstall (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]) #结束应用def Uninstall (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 T Imeout = = 0:return False # You need to configure the ADB environment variable # 1. Make sure there are several phones # 2. Then determine how many apps # 3. Install Mkiller First, start mkiller# 4. Reinstall test Sample # 5. Check if there is a Cancel Install button appears, the description test passed, the test failed to show if __name__ = = "__main__": cleanenv () logger = Util.logger Devicelist = f Inddevices () If Devicelist:apkpath = Os.path.join (OS.GETCWD (), ' apk ') apklist = Util.listfile (APKP ATH) doinstall (devicelist,apklist) #每个手机都要安装apklist里的apk
util.py thread with Execute CMD script function file
#coding: Gbkimport os,sysimport logimport loggingimport threadingimport multiprocessingimport timelogger = log. Logger (' log.log ', clevel = logging. Debug,flevel = logging.info) def exccmd (cmd): Try:return os.popen (cmd). read () except Exception:return None #遍历目录内的文件列表def listfile (Path, isdeep=true): _list = [] If isDeep:try:for root, dirs, fi Les 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 #线程函数class Functhread (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 def run (self): Self.rst = Self.func (*self.params, **self.parammap) self.finished = True def getresult (self): RET Urn Self.rst def isfinished (self): return self.finisheddef Dointhread (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
log.py log corresponding function file
#coding =gbkimport Logging,osimport ctypesforeground_white = 0x0007foreground_blue = 0x01 # text color contains BLUE. foreground_green= 0x02 # text color contains GREEN. foreground_red = 0x04 # text color contains RED. Foreground_yellow = foreground_red | foreground_greenstd_output_handle= -11std_out_handle = Ctypes.windll.kernel32.GetStdHandle (std_output_handle) def Set_color (color, handle=std_out_handle): bool = Ctypes.windll.kernel32.SetConsoleTextAttribute (handle, color) return Boolclass logger:def __init__ (self, path,clevel = logging. Debug,flevel = logging. DEBUG): Self.logger = Logging.getlogger (path) self.logger.setLevel (logging. DEBUG) FMT = logging. Formatter (' [% (asctime) s] [% (levelname) s]% (message) s ', '%y-%m-%d%h:%m:%s ') #设置CMD日志 sh = logging. Streamhandler () sh.setformatter (FMT) sh.setlevel (clevel) #设置文件日志 fh = logging. Filehandler (Path) fh.setformatter (FMT) fh.setlevel (flevel) Self.logger. AddHandler (SH) self.logger.addHandler (FH) def debug (self,message): Self.logger.debug (message) def INF O (self,message): Self.logger.info (Message) def War (Self,message,color=foreground_yellow): Set_color (color ) Self.logger.warn (message) Set_color (foreground_white) def error (self,message,color=foreground_red): Set_color (color) self.logger.error (message) Set_color (foreground_white) def CRI (self,message): Self.logger.critical (message) if __name__ = = ' __main__ ': Logyyx = Logger (' Yyx.log ', logging. Warning,logging. Debug) Logyyx.debug (' A debug Message ') Logyyx.info (' an info message ') Logyyx.war (' A warning message ') logyyx.error (' An error message ') LOGYYX.CRI (' A deadly critical message ')
This small test application although relatively simple, but because just contact Uiautomator python package, so still encountered some trouble, but fortunately, the final result is a good solution to the corresponding problem, here is a point, This uiautomator also has a lot of interesting places to explore, and later to find it slowly ~
Use the Uiautomator Python package for Android UI testing