◆版權聲明:本文出自胖喵~的博客,轉載必須注明出處。
轉載請注明出處:http://www.cnblogs.com/by-dream/p/4996000.html
前言
前面我們已經了解Uiautomator的基本知識,並且學習了API的用法,因此對於我們來說完成一個UI自動化測試腳本並不難,但是如何將UI自動化應用在實際的項目中,幫我們提高測試的效率呢?本節我們就說說,UI自動化應該怎么去完成。
我們以微信"小視屏"這個功能為例,來完成本次自動化測試的講解。(鑒於隱私原因,默認在執行腳本前,微信已經是登錄狀態)
分析
當我們要完成一個自動化時,需要考慮這個用例需要怎么設計,需要測試哪些項,怎么驗證,出現錯誤時應該如何處理。
首先需要明確一點,並不是所有需求文檔上提到的功能,我們都必須用自動化方式去驗證,由於UI自動化本身的局限性,UI自動化的可行度不是100%的准確,因此我們只對“小視屏”的賣點功能進行自動化驗證,你也可以理解為對該功能做一個冒煙測試。
小視屏功能的入口一共是三個,分別是下面這三個地方:
我們除了要驗證這地方的入口外,還需要在其中一處完成對小視屏的發送,並且驗證小視屏發送成功。因此我們可以按照下面流程來進行測試腳本的編寫,流程圖如下所示:
編碼前准備
有了流程圖之后,不要迫不及待的編碼。編碼之前也需要考慮考慮,是否有一些公共的方法可以提取出來做為一個單獨的函數呢?
1、點擊操作
首先,點擊的操作是Uiautomator中用的最多的,而根據控件id和text來做為索引則是更多的。因此我們封裝如下的內容:
/* 定義“通過哪種方式來獲得uiselector”的int標識, 如果以后想添加別的方法(例如 通過description 來獲取),則可以參考此形式進行擴充 */ final int CLICK_ID = 2000; final int CLICK_TEXT = 2001; /* 實現具體的外部可以調用的函數 */ // 通過id來進行點擊操作 public boolean ClickById(String id) { return ClickByInfo(CLICK_ID, id); } // 通過text來進行點擊操作 public boolean ClickByText(String text) { return ClickByInfo(CLICK_TEXT, text); } /* 封裝出通用的點擊方法,供上面的public函數調用 如果以后想添加別的方法(例如 通過description 來獲取),則可以在switch中擴充 */ private boolean ClickByInfo(int CLICK, String str) { UiSelector uiselector = null; // switch根據不同的CLICK標識,創建出UiSelector的對象 switch(CLICK) { case CLICK_ID: uiselector = new UiSelector().resourceId(str); break; case CLICK_TEXT: uiselector = new UiSelector().text(str); break; default: return false; } // 根據UiSelector對象構造出UiObject的對象 UiObject uiobject = new UiObject(uiselector); // 判斷該控件是否存在 if(!uiobject.exists()) { return false; } // 點擊 try { uiobject.click(); } catch (UiObjectNotFoundException e) { e.printStackTrace(); } return true; }
使用上面我的方法封裝之后,你只需要調用 ClickByText("通訊錄"); 即可完成對"通信錄" 這個控件的點擊,並且在因為異常情況獲取不到該控件的時候,也不會報出異常。
然而,我們去點擊一個控件的時候,當它出現找不到的情況的時候,這有可能就是bug了,我們需要將其記錄下來,並且記錄下當時的現場,一般采用截圖的方法,以便我們查問題時候能更直觀的了解到當時機器一個運行情況。因此接下來,我要說說截圖和異常處理。
2、截屏和異常處理
上面的代碼中,當UiObject對象找不到的時候,我們只是返回了一個false,告訴調用者這次調用失敗了,但是為什么失敗,怎么避免這樣的失敗,並沒有記錄下來。因此在這段代碼中,我們需要加以下的內容:
private boolean ClickByInfo(int CLICK, String str) { .... // 判斷該控件是否存在 if(!uiobject.exists()) { TakeScreen(getUiDevice(), str+"-not-find"); return false; } .... } /* 保存屏幕截圖 參數descrip 為 描述該截圖的內容 */ public void TakeScreen(UiDevice device, String descrip) { // 取得當前時間 Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); String datestr = calendar.get(Calendar.HOUR_OF_DAY) + "_" + calendar.get(Calendar.MINUTE) + "_" + calendar.get(Calendar.SECOND); // 保存文件 File files = new File("/mnt/sdcard/"+datestr+"_"+descrip+".jpg"); device.takeScreenshot(files); }
這樣當我們在調用 ClickByText("通訊錄"); 找不到控件的時候,我們的腳本就會自動截取當時屏幕的圖像保存在我們的手機中(如下圖),這樣我們只需打開圖片,就知道當時發生了什么,為什么沒有找到該控件:
看似完美的方案,其實在實際運行中只是幫我們記錄了這個控件這一時刻點擊失敗的原因,而我們想要的是,腳本在調用了這個方法后,盡最大的可能幫我們點擊成功。舉一個簡單的例子:
這是我們寫腳本中經常遇到的一個問題,我們需要 ‘在A頁面上點擊“進入”按鈕,跳轉到B頁面,然后點擊B頁面上的“保存”按鈕’ 完成我們的操作。
一般我們的寫法是:
ClickByText("進入");
ClickByText("保存");
然而當我們的手機特別卡,或者是頁面承載太多東西的時候,當你調用了點擊“進入”按鈕后,B頁面沒有及時的跳轉出來,這個時候調用B頁面上的“保存”按鈕,就會出現異常,而如果你沒有按照我上面的方案去實現的話,系統就會拋出異常,而使用了我上面的方案之后,系統雖然不會拋出異常,而且會在你找不到B頁面的“保存”按鈕時截取當前的屏幕,你完全可以根據截圖來判斷出來:當是沒有找到“保存”按鈕的原因是,當時的B頁面還沒有跳轉出來。然而在這個時候,我最希望的並不是看到日志告訴我說哪里哪里失敗了,而是想讓這次的點擊效果生效。
那么怎么解決這個問題呢?相信很多親手寫過Uiautomator腳本的朋友都知道,在兩個操作直接加如sleep,沒錯,這是解決方案,那么究竟應該slepp多久呢?因為不同的手機響應時間是不一樣的,如果sleep太短就依然存在上述問題;如果sleep太長的話,無疑使得腳本的運行變的緩慢,多出寫無用的sleep。因此我們需要下面的方案解決:
private boolean ClickByInfo(int CLICK, String str) { .... // 判斷該控件是否存在 if(!uiobject.exists()) { TakeScreen(getUiDevice(), str+"-not-find"); return false; } int i = 0; while (!uiobject.exists() && i<5) { sleep(500); if (i== 4) { TakeScreen(getUiDevice(), str+"-not-find");
return false; } i++; } .... }
我們去掉了if判斷的代碼,改為在while循環中等待這個控件的出現,一共等待5次,如果到了第五次,它還沒有出現的話,那么我們就認為它真的不會出現了,這個時候去截屏比第一次就沒有找到更加的有意義。當然如果你還想提高你的UI自動化的健壯性,那么這里還可以加一個類似這樣的函數:
/* 封裝出通用的點擊方法,供上面的public函數調用 如果以后想添加別的方法(例如 通過description 來獲取),則可以在switch中擴充 */ private boolean ClickByInfo(int CLICK, String str) { .... // 判斷該控件是否存在 int i = 0; while (!uiobject.exists() && i<5) { SolveProblems(); sleep(500); if (i== 4) { TakeScreen(getUiDevice(), str+"-not-find"); return false; } i++; } .... } /** * 當進不下去的時候,使用該方法,例如可能是出現了一些對話框遮擋,該方法會把對話框干掉*/ private void SolveProblems() { .... }
這個 SolveProblems() 函數主要是用來解決一些“麻煩”的,例如我們在操作地圖的時候,當gps信號不好的時候,就會彈出下面的對話框:
由於出現的對話框,遮擋住了我們的Activity,影響我們對界面上ui元素的獲取,這個時候,我們就可以在SolveProblems() 加入這樣一斷邏輯:當出現“開啟gps”對話框的時候,就點擊“殘忍的拒絕”,將此對話框給關掉,這樣while的判斷條件再次執行的時候,就可以成功獲取到你想要的元素。下面這段對話主要為了加深你對SolveProblems() 這個函數的理解:
所以說這個SolveProblems()才是提高UI自動化成功率的關鍵,因為每個App都有自己的特征,因此這部分的內容,需要你們在平時的日積月累中才能總結出來,當你有了一個足夠多的經驗庫之后,你的App幾乎不會再因為外界因素而導致失敗了。經過我自己在我項目上的嘗試,效果非常的顯著。
3、日志
日志的重要性不言而喻,當我們在自動化執行的過程中,肯定不會一直盯着屏幕觀察,因此日志使我們最依靠的東西。關於日志的記錄方法多種多樣,我這里提供下我是怎么在Uiautomator中打印日志的:
public String m_logpathString = "/mnt/sdcard/PerformanceLog.txt";
public void UiAutomationLog(String str) { // 取得當前時間 Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); String datestr = calendar.get(Calendar.HOUR_OF_DAY) + ":" + calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND) + calendar.get(Calendar.MILLISECOND) + ":"; FileWriter fwlog = null; try { fwlog = new FileWriter(m_logpathString, true); fwlog.write(datestr + str + "\r\n"); System.out.println(datestr + str); fwlog.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { fwlog.close(); } catch (IOException e) { e.printStackTrace(); } } }
接下來就是把這個函數加在一些關鍵的地方,當出錯的時候,方便我們排查問題即可。下面是腳本打出來的日志格式:
實現
總結上面的所有代碼,我們把這些放到一個公共的方法中,這樣我們的腳本就可以直接引入這個類,然后直接的進行調用這些函數了。公共方法和測試的腳本我們單獨分開,像下面一樣:
首先附上公共方法的完整源代碼:
1 package QQ; 2 3 import java.io.File; 4 import java.io.FileWriter; 5 import java.io.IOException; 6 import java.util.Calendar; 7 8 import com.android.uiautomator.core.UiDevice; 9 import com.android.uiautomator.core.UiObject; 10 import com.android.uiautomator.core.UiObjectNotFoundException; 11 import com.android.uiautomator.core.UiSelector; 12 import com.android.uiautomator.testrunner.UiAutomatorTestCase; 13 14 public class UiautomatorAssistant extends UiAutomatorTestCase 15 { 16 /* UiDevice對象*/ 17 UiDevice mdevice; 18 19 /* log地址 */ 20 public String m_logpathString = "/mnt/sdcard/PerformanceLog.txt"; 21 22 /* 定義“通過哪種方式來獲得uiselector”的int標識, 23 如果以后想添加別的方法(例如 通過description 來獲取),則可以參考此形式進行擴充 */ 24 25 final int CLICK_ID = 2000; 26 final int CLICK_TEXT = 2001; 27 28 /*構造傳入UiDevice對象*/ 29 UiautomatorAssistant(UiDevice device) 30 { 31 mdevice =device; 32 } 33 34 35 /* 實現具體的外部可以調用的函數 */ 36 37 // 通過id來進行點擊操作 38 public boolean ClickById(String id) 39 { 40 return ClickByInfo(CLICK_ID, id); 41 } 42 // 通過text來進行點擊操作 43 public boolean ClickByText(String text) 44 { 45 return ClickByInfo(CLICK_TEXT, text); 46 } 47 48 /* 封裝出通用的點擊方法,供上面的public函數調用 49 如果以后想添加別的方法(例如 通過description 來獲取),則可以在switch中擴充 */ 50 private boolean ClickByInfo(int CLICK, String str) 51 { 52 UiSelector uiselector = null; 53 // switch根據不同的CLICK標識,創建出UiSelector的對象 54 switch(CLICK) 55 { 56 case CLICK_ID: uiselector = new UiSelector().resourceId(str); break; 57 case CLICK_TEXT: uiselector = new UiSelector().text(str); break; 58 default: return false; 59 } 60 // 根據UiSelector對象構造出UiObject的對象 61 UiObject uiobject = new UiObject(uiselector); 62 // 判斷該控件是否存在 63 int i = 0; 64 while (!uiobject.exists() && i<5) 65 { 66 SolveProblems(); 67 sleep(500); 68 if (i== 4) 69 { 70 TakeScreen(str+"-not-find"); 71 return false; 72 } 73 i++; 74 } 75 // 點擊 76 try 77 { 78 UiAutomationLog("click type:"+CLICK+" content:"+str ); 79 uiobject.click(); 80 } catch (UiObjectNotFoundException e) 81 { 82 e.printStackTrace(); 83 } 84 return true; 85 } 86 87 /* 當進不下去的時候,使用該方法,例如可能是出現了一些對話框遮擋,該方法會把對話框干掉 */ 88 private void SolveProblems() 89 { 90 91 } 92 93 /* 保存屏幕截圖 94 參數descrip 為 描述該截圖的內容 */ 95 public void TakeScreen(String descrip) 96 { 97 // 取得當前時間 98 Calendar calendar = Calendar.getInstance(); 99 calendar.setTimeInMillis(System.currentTimeMillis()); 100 String datestr = calendar.get(Calendar.HOUR_OF_DAY) + "_" + calendar.get(Calendar.MINUTE) + "_" + calendar.get(Calendar.SECOND); 101 102 // 保存文件 103 File files = new File("/mnt/sdcard/"+datestr+"_"+descrip+".jpg"); 104 UiAutomationLog("TakeScreen: " + datestr+"_"+descrip+".jpg"); 105 mdevice.takeScreenshot(files); 106 } 107 108 /* 打log記錄在手機中 */ 109 public void UiAutomationLog(String str) 110 { 111 // 取得當前時間 112 Calendar calendar = Calendar.getInstance(); 113 calendar.setTimeInMillis(System.currentTimeMillis()); 114 String datestr = calendar.get(Calendar.HOUR_OF_DAY) + ":" + calendar.get(Calendar.MINUTE) + ":" + calendar.get(Calendar.SECOND) + calendar.get(Calendar.MILLISECOND) + ":"; 115 116 FileWriter fwlog = null; 117 try 118 { 119 fwlog = new FileWriter(m_logpathString, true); 120 fwlog.write(datestr + str + "\r\n"); 121 System.out.println(datestr + str); 122 fwlog.flush(); 123 124 } catch (IOException e) 125 { 126 e.printStackTrace(); 127 } finally 128 { 129 try 130 { 131 fwlog.close(); 132 } catch (IOException e) 133 { 134 e.printStackTrace(); 135 } 136 } 137 } 138 }
這個類需要注意的就是31行這里的UiDevice對象需要從測試類中傳遞過來。否則無法得到UiDevice對象。
好,接下來我們看看最終實現的腳本的源碼:
1 package QQ; 2 3 import java.io.IOException; 4 5 import com.android.uiautomator.core.UiObject; 6 import com.android.uiautomator.core.UiObjectNotFoundException; 7 import com.android.uiautomator.core.UiSelector; 8 import com.android.uiautomator.testrunner.UiAutomatorTestCase; 9 10 public class Test_wechat extends UiAutomatorTestCase 11 { 12 13 UiautomatorAssistant uiautomatorAssistant ; 14 15 public void testDemo() throws IOException, UiObjectNotFoundException { 16 17 uiautomatorAssistant = new UiautomatorAssistant(getUiDevice()); 18 19 // 啟動App 20 Runtime.getRuntime().exec("am start com.tencent.mm/com.tencent.mm.ui.LauncherUI"); 21 sleep(3000); 22 23 /*----------------------- 驗證第一種小視頻的打開方式------------------------------------*/ 24 uiautomatorAssistant.ClickByText("通訊錄"); 25 // 點擊一個好友 26 uiautomatorAssistant.ClickById("com.tencent.mm:id/gx"); 27 // 點擊發消息 28 uiautomatorAssistant.ClickByText("發消息"); 29 // 點擊發送欄的“+” 30 uiautomatorAssistant.ClickById("com.tencent.mm:id/wm"); 31 // 點擊小視頻 32 uiautomatorAssistant.ClickByText("小視頻"); 33 // 驗證第一種小視頻打開方式 34 UiObject obj_anzhupaiObject = new UiObject(new UiSelector().text("按住拍")); 35 if (obj_anzhupaiObject.exists()) 36 { 37 uiautomatorAssistant.UiAutomationLog( "第一次進入小視頻的方法測試通過"); 38 } 39 else { 40 uiautomatorAssistant.TakeScreen("第一次進入小視頻的方法測試不通過"); 41 } 42 43 44 // 第二種、第三種類似第一種的方法,這里省略。 45 // 在我代碼還沒完成的時候,微信發出了新版本,屏蔽了第三種下拉的方式,打開小視頻。 46 } 47 48 }
很明顯可以看到,使用了封裝函數之后,代碼的可讀性大大的增強,而且很好的維護,要完成一個其他的case也可以輕而易舉。后續如果讀者朋友有需要完成Uiautomator腳本的不妨可以使用我的公共類來輔助你完成你的腳本,這樣你就可以更加高效,快速的完成一個失敗率降到最低的UI自動化測試腳本。