1. 程式人生 > >如何利用 UIAutomation 自動化測試 iOS 應用

如何利用 UIAutomation 自動化測試 iOS 應用

翻譯正文:

快速入門

自動化測試程式碼可以“在你的睡著的時候”很好地幫你測試你的應用程式。它可以讓你能夠快速地跟蹤你程式中的迴歸和效能方面的問題,這樣你就不用擔心你新增的功能會影響到你之前已經完成開發的程式了。

隨著iOS4.0的釋出,蘋果公司同時釋出了一個名為UIAutomation的測試框架,它可以用來在真實裝置和iPhone模擬器上執行自動化測試。但官方關於UIAutomation的文件相當的有限,在網路上也沒有太多的資源可以查詢的。本文將向你展示你如何將UIAutomation整合到你的工作流程當中去。

作為基礎知識的準備,你可以先看一下蘋果公司關於UIAutomation的文件,另外還有一篇快速入門的介紹蘋果Instruments的文件也值得看看,當然,如果你有一個免費的Apple開發者賬號的話,你可以看一下WWDC 2010 - Session 306 – 使用Instruments進行使用者介面自動化測試的幻燈片或者視訊。

除此之外,包括在Xcode中的OCUnit測試框架也可以用來為你的應用程式編寫單元測試。

1. 第一個UIAutomation測試指令碼

使用iOS 模擬器
使用iOS裝置

2. 處理UIAElement和元素可訪問性(Accessibility)

UIAElement層次結構
模擬使用者操作

3. 經驗分享(讓你的生活變得更簡單)

類庫Tune-up介紹
匯入外部指令碼
使用強大的命令列
使用錄製互動功能
當遇到問題時,加上“UIATarget.delay(1);”

4. 高階互動

處理非預期和預期的提示框(alerts)
多工
螢幕方向
截圖
載入本地指令碼

5. 總結

有用的連結
一個視訊

1. 你的第一個UIAutomation測試指令碼

UIAutomation的功能測試程式碼是用Javascript編寫的。UIAutomation和Accessibility有著直接的關係,你將用到通過標籤和值的訪問性來獲得UI元素,同時完成相應的互動操作。

下面讓我們來編寫我們的第一段測試程式碼。

使用iOS模擬器

  1. 下載示例應用程式TestAutomation.xcodeproj,並開啟它。這個專案是一個很簡單的包含2個tab的tabbar應用程式。

  2. 確保選中如下圖所示的“TestAutomation > iPhone 5.0 Simulator”模式(或許你已經切換成5.1了,因此它可能是iPhone5.1模擬器)。

  3. 啟動Instruments(Product > Profile),或者通過⌘I。

  4. 選擇左邊的iOS Simulator,然後再選擇Automation模板,然後點選“Profile”。

  5. Instruments就已經啟動好後,然後直接開始錄製了。這裡先停止錄製,(紅包按鈕或者⌘R)。

  6. 在左邊的Scripts視窗,點選“Add > Create”建立新的指令碼。

  7. 在指令碼編輯器裡,輸入下面的程式碼

var target = UIATarget.localTarget();
var app = target.frontMostApp();
var window = app.mainWindow();
target.logElementTree();

8.重新執行這段指令碼⌘R(不需要儲存)。指令碼跑起來後,你可以在日誌打完後停止它。

贊一個!我們就這樣完成了我們的第一個UIAutomation測試用例。

使用iOS裝置

你除了將你的測試用例執行模擬器上,也可以將它執行在一個真實的裝置上。不過,自動化測試用例只能執行在支援多工的:iPhone 3GS,iPad,iOS > 4.0等裝置上。遺憾的是不管iPhone 3G的系統版本是什麼,都不支援。

下面是如何操作:

  1. 通過USB介面連線上你的iPhone。

  2. 選擇 “TestAutomation > iOS Device”模式。

  3. 確保Developper profile設定成Release模式(而不是Ad-Hoc Distribution profile)。預設情況下,profiling是設定成Release模式的(因為沒有必要將profile設定成Debug模式)。

  4. 啟動測試 (⌘I)

  5. 後面的步驟請參考前面模擬器部分。

2. 處理UIAElement和元素可訪問性(Accessibility)

UIAElement層次結構

Accessibility和UIAutomation有密切的聯絡:如果一個控制元件的Accessibility是可以被訪問的,你就可以設定和讀取它的值,作相關的操作,而當一個控制元件的Accessibility不可見時,你就沒有辦法通過automation訪問它。

你可以通過Interface Builder,或者通過在程式裡設定isAccessibilityElement屬性的方式來設定一個控制元件的Accessibility或者可被自動化。當你設定container view(即:一個檢視包含其它的UIKit元素)的accessibility時,你必須注意。你設定了整個View的accessibility將會“隱藏”它的子檢視的accessibility,例如:在示例專案中,你不能將outlet檢視設定成可訪問的,否則它所有的子控制元件將都不可以訪問了。在任何時候,logElementTree都是你忠實的朋友:它將當前介面的所有可被訪問的元素都列印在日誌裡。

每一個可以被訪問的UIKit控制元件都可以用一個Javascript物件來描述,它就是一個UIAElement。UIAElement有幾個屬性:name, value, elements, parent。你的主視窗包含很多的控制元件,它們是以UIKit層次的方式定義的,這些UIKit層次結構對應的是UIAElement的層次樹。例如:前面的測試程式碼中,通過呼叫logElementTree,我們可以得到如下面所示的樹結構:

+- UIATarget: name:iPhone Simulator rect:{{0,0},{320,480}}

| +- UIAApplication: name:TestAutomation rect:{{0,20},{320,460}}

| | +- UIAWindow: rect:{{0,0},{320,480}}

| | | +- UIAStaticText: name:First View value:First View rect:{{54,52},{212,43}}

| | | +- UIATextField: name:User Text value:Tap Some Text Here ! rect:{{20,179},{280,31}}

| | | +- UIAStaticText: name:The text is: value:The text is: rect:{{20,231},{112,21}}

| | | +- UIAStaticText: value: rect:{{145,231},{155,21}}

| | | +- UIATabBar: rect:{{0,431},{320,49}}

| | | | +- UIAImage: rect:{{0,431},{320,49}}

| | | | +- UIAButton: name:First value:1 rect:{{2,432},{156,48}}

| | | | +- UIAButton: name:Second rect:{{162,432},{156,48}}

你可以通過下面的程式碼來訪問文字框:

var textField = 
UIATarget.localTarget().frontMostApp().mainWindow().textFields()[0];

你可以選擇通過從0開始的索引或者這個元素的名稱來訪問這個元素,例如:你也可以通過下面的程式碼來訪問文字控制元件。

var textField = 
UIATarget.localTarget().frontMostApp().mainWindow().textFields()["User Text"];

後一種方式更加清晰明瞭,應該多使用。你可以通過Interface Builder設定UIAElement的name屬性,

或者通過編寫程式碼的方式:

myTextField.accessibilityEnabled = YES;
myTextField.accessibilityLabel = @"User Text";

你現在可以看到,通過accessibility屬性可以被UIAutomation用來找到不同的控制元件。這非常的清晰,因為,第一,你只要學習一個測試框架;第二,通過編寫自動化測試程式碼,你同時還可以保證你的程式是可以被訪問的。因此,每一個UIAElement物件的子控制元件可以通過下面的方法進行訪問:

buttons(), images(), scrollViews(),textFields(), webViews(), segmentedControls(), sliders(), staticTexts(), switches(), tabBar(),tableViews(), textViews(), toolbar(), toolbars() 等等……

你可以通過如下程式碼在tabbar上訪問第一個tab:

var tabBar = UIATarget.localTarget().frontMostApp().tabBar();
var tabButton = tabBar.buttons()["First"];

UIAElement結構層次非常的重要,你以後會常常用到它。而且你還要記住,你可以在隨時通過呼叫UIAAplication的logElementTree來獲得它的結構。

UIATarget.localTarget().frontMostApp().logElementTree();

在模擬器上,你還可以啟用Accessibility 的檢測器。啟動模擬器,找到“Settings > General > Accessibility > Accessibility Inspector”,然後將它設為“開啟”狀態。

這個彩色的小框框就是Accessibility 檢測器了。當它收起來的時候,Accessibility就被關閉了,當它展開的時候,Accessibility就是開啟的。你只要點選上面的箭頭按鈕就可以啟用或者遮蔽Accessibility。現在,開啟我們的示例程式,啟用檢測器。

然後,點選文字框,檢查UIAElement的name和value屬性(其實就是accessibilityLabel和accessibilityValue對應的NSObject型別的值)。這個檢測器可以幫助你除錯和編寫你的測試程式碼。

模擬使用者操作

讓我們更進一步,模擬一些使用者的互動操作。你可以簡單地呼叫按鈕的tap()來作一個點選操作:

var tabBar = UIATarget.localTarget().frontMostApp().tabBar();
var tabButton = tabBar.buttons()["First"];   

// Tap the tab bar !
tabButton.tap();

你還可以呼叫UIAButtons的doubleTap(), twoFingerTap()。如果你不想操作具體的某個元素,你也可以直接根據螢幕上指定的座標點進行操作,你可以這麼用:

點選:

UIATarget.localTarget().tap({x:100, y:200});
UIATarget.localTarget().doubleTap({x:100, y:200});
UIATarget.localTarget().twoFingerTap({x:100, y:200});

縮放:

UIATarget.localTarget().pinchOpenFromToForDuration({x:20, y:200},{x:300, y:200},2);
UIATarget.localTarget().pinchCloseFromToForDuration({x:20, y:200}, {x:300, y:200},2);

拖拽與划動:

UIATarget.localTarget().dragFromToForDuration({x:160, y:200},{x:160,y:400},1);
UIATarget.localTarget().flickFromTo({x:160, y:200},{x:160, y:400});

注意,當你指定操作的時間間隔的時候,它是有特定的範圍的,即:拖拽操作的時間間隔必須大於或者等於0.5秒,小於60秒。

現在,讓我們來練習一下:

停止Instruments (⌘R)
在Scripts窗口裡, 移除當前的指令碼
點選“Add > Import”然後選擇TestAutomation/TestUI/Test-1.js(將下面的程式碼儲存到這個路徑)
點選錄製按鈕 (⌘R) 然後看看將會發生什麼…
下面是Test-1.js程式碼:

var testName = "Test 1";
var target = UIATarget.localTarget();
var app = target.frontMostApp();
var window = app.mainWindow();
UIALogger.logStart( testName );
app.logElementTree();
//-- select the elements
UIALogger.logMessage( "Select the first tab" );
var tabBar = app.tabBar();
var selectedTabName = tabBar.selectedButton().name();
if (selectedTabName != "First") {
    tabBar.buttons()["First"].tap();
}
//-- tap on the text fiels
UIALogger.logMessage( "Tap on the text field now" );
var recipeName = "Unusually Long Name for a Recipe";
window.textFields()[0].setValue(recipeName);
target.delay( 2 );
//-- tap on the text fiels
UIALogger.logMessage( "Dismiss the keyboard" );
app.logElementTree();
app.keyboard().buttons()["return"].tap();
var textValue = window.staticTexts()["RecipeName"].value();
if (textValue === recipeName){
    UIALogger.logPass( testName ); 
}
else{
    UIALogger.logFail( testName ); 
}

這段指令碼先啟動待測程式,然後,如果第一個tab沒有被選的話就切換到第一個tab,並將上面的文字框的值設成“Unusually Long Name for a Recipe”,接著收起虛擬鍵盤。這裡有一些新的方法值得注意的:UIATarget的delay(Number timeInterval) 方法允許你在兩個操作之間做一些等待,UIALogger的logMessage( String message) 方法用來將你想列印的資訊輸出到日誌上去,UIALogger的logPass(String message)方法指明你的測試指令碼已經成功的完成測試了。

你還知道了如何訪問鍵盤上的按鈕,然後作點選操作:

app.keyboard().buttons()["return"].tap();

3. 經驗分享(讓你生活變得更簡單)

類庫Tune-up介紹

現在你應該基本上知道如何編寫測試程式碼了。但你慢慢地會發現,你會經常寫到一些重複的,冗餘的,黏糊糊的程式碼,像下面一樣:

var target = UIATarget.localTarget();
var app = target.frontMostApp();
var window = app.mainWindow();

這也是為什麼我們會用到一個小的Javascript類庫來簡化我們寫的UIAutomation測試的原因。你可以去 https://github.com/alexvollmer/tuneup_js獲取這個類庫,然後將它複製到你的測試目錄下面。現在讓我們使用Tune-Up類庫來重新編寫我們的Test1.js:

#import "tuneup/tuneup.js"

test("Test 1", function(target, app) {
    var window = app.mainWindow();
    app.logElementTree();

    //-- select the elements
    UIALogger.logMessage( "Select the first tab" );
    var tabBar = app.tabBar();
    var selectedTabName = tabBar.selectedButton().name();

    if (selectedTabName != "First") {
        tabBar.buttons()["First"].tap();
    }

    //-- tap on the text fiels
    UIALogger.logMessage( "Tap on the text field now" );
    var recipeName = "Unusually Long Name for a Recipe";
    window.textFields()[0].setValue(recipeName);
    target.delay( 2 );

    //-- tap on the text fiels
    UIALogger.logMessage( "Dismiss the keyboard" );
    app.logElementTree();
    app.keyboard().buttons()["return"].tap();
    var textValue = window.staticTexts()["RecipeName"].value();
    assertEquals(recipeName, textValue);
});

Tune-Up可以避免你編寫重複的程式碼,同時還給你提供了各種好用的斷言方法:

assertTrue(expression, message),
assertMatch(regExp, expression, message),
assertEquals(expected, received, message),
assertFalse(expression, message),
assertNull(thingie, message),
assertNotNull(thingie, message),
assertNull(thingie, message),
assertNotNull(thingie, message)
等等

你也可以很容易的擴充套件這個類庫:例如,你可以通過將方法加入到uiautomation-ext.js:裡面來為UIATarget類加一個logDevice方法:

extend(UIATarget.prototype, {
        logDevice: function(){
        UIALogger.logMessage("Dump Device:");
        UIALogger.logMessage(" model: " + UIATarget.localTarget().model());
        UIALogger.logMessage(" rect: " + JSON.stringify(UIATarget.localTarget().rect()));
        UIALogger.logMessage(" name: "+ UIATarget.localTarget().name());
        UIALogger.logMessage(" systemName: "+ UIATarget.localTarget().systemName());
        UIALogger.logMessage(" systemVersion: "+ UIATarget.localTarget().systemVersion());
    }
});

然後當你呼叫target.logDevice()就可以看到:

Dump Device:
model: iPhone Simulator
rect: {"origin":{"x":0,"y":0},"size":{"width":320,"height":480}}
name: iPhone Simulator

匯入外部指令碼

你也可以看到如何在一個指令碼檔案裡引用另一個指令碼,即通過#import指令。因此,你可以建立多個測試檔案,然後將它們通過匯入到單個檔案的方式來連結並呼叫它們:

#import "Test1.js"
#import "Test2.js"
#import "Test3.js"
#import "Test4.js"
#import "Test5.js"

使用強大的命令列

如果你想讓你的測試程式碼自動的執行起來,你還可以通過命令列來啟動測試。其實,我比較推薦這種方式,而不是使用Instruments的圖形介面程式。因為,Instruments的圖形介面程式比較慢,而且即使你的測試程式碼跑完了它也還是會一直執行著。而通過命令列來啟動和執行測試程式碼更快,它會在跑完測試後自動的停止。

為了可以在命令列終端執行你的指令碼,你需要知道你裝置的UDID和型別:

instruments -w your_ios_udid -t 
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Instruments/PlugIns/AutomationInstrument.bundle/Contents/Resources/Automation.tracetemplate 
name_of_your_app -e UIASCRIPT absolute_path_to_the_test_file

例如,使用我自己的機子,就這麼寫的:

instruments -w a2de620d4fc33e91f1f2f8a8cb0841d2xxxxxxxx -t 
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Instruments/PlugIns/AutomationInstrument.bundle/Contents/Resources/Automation.tracetemplate 
TestAutomation -e UIASCRIPT 
/Users/jc/Documents/Dev/TestAutomation/TestAutomation/TestUI/Test-2.js

如果你使用的Xcode版本低於4.3的話,你需要這樣寫:

instruments -w your_ios_device_udid -t /Developer/Platforms/iPhoneOS.platform/Developer/Library/Instruments/PlugIns/AutomationInstrument.bundle/Contents/Resources/Automation.tracetemplate TestAutomation -e UIASCRIPT /Users/jc/Documents/Dev/TestAutomation/TestAutomation/TestUI/Test-2.js

一個小提示,不要忘了關閉你裝置的密碼驗證,否則你會看到這樣的日誌資訊的:remote exception encountered : ’device locked : Failed to launch process with bundle identifier ’com.manbolo.testautomation’. 的確,因為UIAutomation根本不知道你的密碼啊。

命令列終端同樣可以在模擬器上使用,但你需要知道待測應用程式在檔案系統中的絕對路徑。模擬器將目錄~/Library/Application Support/iPhone Simulator/5.1/ “模擬”成了裝置的檔案系統。在這個目錄下,你可以找到一個包含裝在模擬器上的所有應用程式的沙盒的Applications資料夾。定位到TestAutomation程式的目錄,然後:

instruments -t /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Instruments/PlugIns/AutomationInstrument.bundle/Contents/Resources/Automation.tracetemplate "/Users/jc/Library/Application Support/iPhone Simulator/5.1/Applications/C28DDC1B-810E-43BD-A0E7-C16A680D8E15/TestAutomation.app" -e UIASCRIPT /Users/jc/Documents/Dev/TestAutomation/TestAutomation/TestUI/Test-2.js

最後,如果你沒有指定日誌輸入到哪裡的話,你的測試結果會被放到你命令行當前指定(工作)的目錄下。你可以通過加入 -e UIARESULTSPATH results_path 引數來指定日誌輸入目錄。

我沒有成功的將多個測試指令碼並行著在命令列中執行起來。但是你可以將你的測試指令碼串連進來,有一整晚去跑它,這樣就真正的實現了“在你睡著的時候”,就完成了對應用程式的測試。

使用錄製互動功能

除了手動的編寫指令碼,你還可以直接在裝置上或者模擬器上錄製指令碼,然後替換掉原來的。下面是步驟:

  1. 啟動Instruments (⌘I)

  2. 建立一個新的指令碼

  3. 選擇指令碼編輯器

  4. 在指令碼編輯器的底端,你是否看到了一個紅色的按鈕?點選它!

  5. 現在,你可以操作你的應用程式;你將看到錄製的互動操作出現在指令碼視窗(甚至旋轉事件)。點選方形按鈕來停止錄製。

當遇到問題時,加上“UIATarget.delay(1);”

當你在編寫指令碼的時候,你總是在與時間,動畫打交道。UIAutomation有很多方式去獲取控制元件元素,然後等待它們變為可用狀態,即使有時候它們還沒有顯示出來,但根據這篇文件裡提到的,最好的建議是:

當遇到問題時,加上UIATarget.delay(1);!

4. 高階互動

處理非預期和預期的提示框(alerts)

在寫自動化測試過程中,處理提示框是很難的一件事情:你已經很認真的寫好了你的測試用例,然後在你準備睡覺之前將它跑起來,然後,到第二天早上,你發現你的測試用例被一個未知訊息提示框給毀了。然而,UIAutomation幫助你處理了這種情況。

通過下面程式碼來實現:

UIATarget.onAlert = function onAlert(alert){
    var title = alert.name();
    UIALogger.logWarning("Alert with title ’" + title + "’ encountered!");
    return false; // use default handler
}

它返回一個false,UIAutomation會自動的幫你銷燬UIAlertView視窗,因此提示框就不會再影響你的測試了。你的測試指令碼就永遠不會有提示框彈出了。但是提示框可能是你應用程式的一部分,涉及到你測試的流程,所以,有時候,你不希望它被自動的處理掉。這時,你可以根據提示框的標題來決定,點選某個按鈕,然後返回true。通過返回true,你向UIAutomation指定這個提示框必須作為測試的一部分來考慮。

例如,如果你想當提示框的標題為“Add Something”時,點選“Add”按鈕,你可以這麼寫:

UIATarget.onAlert = function onAlert(alert) {
    var title = alert.name();
    UIALogger.logWarning("Alert with title ’" + title + "’ encountered!");
    if (title == "Add Something") {
        alert.buttons()["Add"].tap();
        return true; // bypass default handler
    }
    return false; // use default handler
}

容易吧?

多工

測試你的應用程式的多工是非常容易的事:假設你想測試每次啟動一個瘋狂的後臺程序,將程式放到後臺執行,並進入(void)applicationWillEnterForeground:(UIApplication *)application選擇器程式碼段,你可以通過下面的程式碼來將它推至後臺,然後等待10秒後自動返回活動狀態。.

UIATarget.localTarget().deactivateAppForDuration(10);

deactivateAppForDuration(duration) 方法會暫停測試指令碼的執行,模擬使用者點選Home按鈕,(即將程式放到後臺),等待,然後為你重新啟用程式和測試指令碼,就這麼一行程式碼而已!

螢幕方向

最後,你可以模擬你的iPhone的旋轉方向。也是很直觀很簡單:

var target = UIATarget.localTarget();
var app = target.frontMostApp();
// set landscape left
target.setDeviceOrientation(UIA_DEVICE_ORIENTATION_LANDSCAPELEFT);
UIALogger.logMessage("Current orientation is " + app.interfaceOrientation());
// portrait
target.setDeviceOrientation(UIA_DEVICE_ORIENTATION_PORTRAIT);
UIALogger.logMessage("Current orientation is " + app.interfaceOrientation());

截圖
你還可以對 APP的當前狀態輕鬆截圖。可以將截圖與預期進行對比。

var target = UIATarget.localTarget();
target.captureScreenWithName( "screenshot1.png" ); 

截圖存放的地址需要設定下環境變數:

option -e UIARESULTSPATH results_path

載入本地指令碼
最後,你可以載入任何(不僅 js)本地的指令碼。與截圖結合起來,你可以想象到自動化測試的威力。你可以用

 performTaskWithPathArgumentsTimeout(path, args, timeout)

path包含指令碼全路徑,args 是給你的指令碼傳參,timeout是設定超時時間。

var target = UIATarget.localTarget();
var host = target.host();

var result = host.performTaskWithPathArgumentsTimeout("/usr/bin/echo", ["Hello World"], 5);

5. 總結

有用的連結

這篇文章有點兒長,但我希望你們能見識到UIAutomation是很強大的,且你的應用程式是可以得到質量的保障的。網上沒有太多的UIAutomation的資料,但我還是列出了很多連結,也許能幫到你。

一個視訊

在介紹完UIAutomation之後,我不反對向你們展示我們是如何使用在Meon中使用 UIAutomation的一段小視訊。我們使用了各種測試,在這個視訊中,我們測試了玩家可以從0級玩到120級。幫幫我,我的iPhone還活著呢!