Appium 從入門到原理

bestswifter發表於2017-03-11

因為業務需求和準備畢設,最近開始研究自動化測試的內容。由於同時要做 iOS、安卓和 Web 測試,我們最終選擇了 Appium 這個開源工具並基於它做一些封裝,從而能夠使用一套公共 API 完成移動端的雙端測試。本文主要會基於一些開原始碼和個人實踐,對 iOS 端的自動化測試原理做一個簡單介紹,Android 略有區別但也大致同理。

其實文章沒有很長,也沒有太多技術含量,驅使我寫這篇文章的主要原因是 Google 上能搜到的絕大多數部落格都是錯的,大多是根據一篇老舊過時的文章抄抄改改。所以真的很想問問這些文章的作者,你真的搞懂 Appium 的原理麼?對這一些錯誤的知識有可能搞懂麼?自己不先搞懂,怎麼能昧著良心寫進部落格裡?

Appium

我假設讀者完全沒有了解過自動化測試以及相關的概念,那麼首先就要搞明白 Appium 是什麼,大概由幾個步驟組成,接下來才是對每個部分的深入瞭解。

簡單來說,Appium 是一個測試工具,可以進行 iOS、Android 和 Web 測試,同時還允許使用多種語言來編寫測試用例。那麼問題就變成了為什麼 Appium 支援多種語言來寫測試用例,以及這些測試用例是如何執行在具體的平臺(比如 iOS )上的。為了回答這個問題,我們需要把 Appium 分成三個部分來看,分別是 Appium 客戶端、Appium 服務端和裝置端。既然是自動化測試,那麼就先從裝置端說起。

裝置端

如果你按照官網的教程成功的執行了 iOS 真機測試,你會看到手機上多了一個名為 WebDriverAgentRunner 的應用,以後簡稱 WDA,這個應用的作用就是對你的目標 App 進行測試。

好吧,是不是覺得事情有點神奇了?安裝了一個別人的 app,居然能喚起你自己的應用,還能執行測試,是不是存在什麼黑魔法?

首先需要介紹一下蘋果的 UI 自動化測試框架,在 Xcode 7 以前使用了UI Automation 框架,利用 JS 指令碼去做應用測試。而在 Xcode 7 中蘋果提供了新的框架 UI Testing,在 Xcode 8 中乾脆直接移除了對 UI Automation 的支援。所以毫無疑問,在 iOS 9 或者更高的系統版本中,Appium 也是利用了 UI Testing 框架來做測試而不是UI Automation

很多程式設計師應對 UI Testing 框架並不陌生,在新建專案的時候就有機會勾選上這個選項,或者後期通過 Add target 的方式補上。預設情況下,一個測試用例就是一個 .m 檔案,模板程式碼如下:

#import <XCTest/XCTest.h>

@interface Test : XCTestCase

@end

@implementation Test

- (void)setUp {
    [super setUp];

    // Put setup code here. This method is called before the invocation of each test method in the class.

    // In UI tests it is usually best to stop immediately when a failure occurs.
    self.continueAfterFailure = NO;
    // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
    [[[XCUIApplication alloc] init] launch];

    // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}

- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

- (void)testExample {
    // Use recording to get started writing UI tests.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}
@end複製程式碼

可以看到一共只有三個方法, setUp 方法中主要做一些測試前的準備,比如這裡的 [[[XCUIApplication alloc] init] launch]; 就建立了一個被測試應用的例項並喚起它。tearDown 方法是測試結束後的清理工作。

所有的測試函式都必須以 test 開頭,比如這裡的 - (void)testExample。好吧,不得不承認 OC 這們語言還是有缺陷的,缺少了 Annotation 以後就只能用變數名來做標記,這種業務對應的 Java 表示應該是:

@test
public void example() {
    // do some test
}複製程式碼

言歸正傳,有了這樣的測試程式碼後,只要 Command + U 就可以執行測試。不過這還是沒有解決之前的疑惑,為什麼 Appium 可以用一個第三方 app 喚起待測試的應用並進行除錯?當然,這個問題等價於,上面程式碼中的 [[XCUIApplication alloc] init] 到底會建立一個什麼樣的 app 例項?它怎麼知道這個物件的 launch 方法會開啟手機上的哪個 app?

這個問題似乎沒有搜到比較明確的答案,不過經過實踐分析以後發現,XCUIApplication 類存在一個私有方法,可以傳入目標應用的 BundleID:

[[XCUIApplication alloc] initPrivateWithPath:nil bundleID:@"com.bestswifte.targetapp"];複製程式碼

我們知道手機上一個 BundleID 唯一對應了一個應用,後裝的應用會替換掉之前相同 ID 的應用,所以通過 BundleID 總是可以正確的喚起待測試應用。唯一要注意的是,為了順利通過編譯器的語法檢測,我們在呼叫私有方法之前需要先構造一份 XCUIApplication 的標頭檔案,宣告一下將要呼叫的私有方法。

當我們用這個私有的初始化方法替換掉預設的 init 方法後,就可以正常喚起待測試應用了,不過你會發現被測試的應用剛一開啟就會退出,這是因為我們的測試程式碼內容為空,所以很快就會進入到銷燬流程。

解決問題也很簡單,我們可以在 testExample 裡面跑一個死迴圈,模擬 Runloop 的操作。只不過這次不是監聽使用者事件,而是監聽某個 TCP 埠,等待網路傳輸過來的訊息。

我們以 Facebook 開源的 WDA 為例,看看它的 FBScreenshotCommands.m 檔案:

#import "FBScreenshotCommands.h"

#import "XCUIDevice+FBHelpers.h"

@implementation FBScreenshotCommands

#pragma mark - <FBCommandHandler>

+ (NSArray *)routes
{
  return
  @[
    [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)],
    [[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)],
  ];
}


#pragma mark - Commands

+ (id<FBResponsePayload>)handleGetScreenshot:(FBRouteRequest *)request
{
  NSString *screenshot = [[XCUIDevice sharedDevice].fb_screenshot base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
  return FBResponseWithObject(screenshot);
}

@end複製程式碼

這裡首先會註冊一個 /screenshot 的路由,並且制定處理函式為 handleGetScreenshot,然後函式內部呼叫 XCUIDevice 的截圖方法。

所以分析到這裡,WDA 的套路就很清晰了,它能根據被測試應用的 BundleID 將它喚起,然後自己進入死迴圈保證測試用例一直不退出。此時等待伺服器傳來 URL 資料,然後解析 URL,分發到對應模組,各個模組根據 URL 指令執行對應的測試操作,最終再把測試結果返回。

Appium 服務端

簡單來說,Appium 服務端是一個 Node.js 應用,這個應用跑在電腦上,用於和 WDA 進行通訊。剛剛我們看到了截圖命令的 URL 是 /screenshot,可以想見還有別的類似的測試操作,所以 WDA 有必要和 Appium 服務端約定一套通訊協議。考慮到 Appium 還支援 Android 測試,所以在安卓手機上也有類似的東西需要和 Appium 服務端進行互動。這樣一來,約定一套通用的協議就顯得非常重要。

Appium 採用的是 WebDriver 協議。在 w3.org 上有一個對該協議的詳細描述,而 Selenim 的官網 也介紹了 WebDriver 協議。目前我尚不知道這兩處介紹的關係,是互為補充 or 兩套規範,但可以肯定的是下面這段話的介紹:

WebDriver’s goal is to supply a well-designed object-oriented API that provides improved support for modern advanced web-app testing problems.

所以簡單的把 WebDriver 理解成一套通用的測試協議即可。

Appium 客戶端

Appium 客戶端就是指我們寫的那些測試程式碼了。Appium 支援多種測試語言的根本原因在於,WebDriver 協議為各種主流語言提供了一個第三方庫,能夠方便的把測試指令碼轉化成符合 WebDriver 規範的 URL。比如 www.w3.org/TR/webdrive… 就規定了包括截圖、尋找元素、點選元素等操作對應的 URL 格式。

我們的專案目前使用 Java 語言來編寫測試指令碼,這樣的好處是 Android 工程師就可以承擔起維護和編寫 Android、iOS 兩個平臺下測試程式碼的重任了。

總結

其實 Appium 的原理非常簡單,一句話就能概括:

提供各個語言的第三方庫,將測試指令碼轉化成 WebDriver 協議下的 URL,通過 Node 服務傳送到各個平臺上的代理工具,代理工具在執行過程中不斷接收 URL,根據 WebDriver 協議解析出要執行的操作,然後呼叫各個平臺上的原生測試框架完成測試,再將測試結果返回給 Node 伺服器。

最後再友情提醒一句:

iOS 上的真機測試環境很難配置,如果一天搞不定,不要氣餒,多試幾次,也許明天就好了

參考資料:

  1. [iOS9 UIAutomation] What is Appium approach to UIAutomation deprecation by Apple
  2. 現在開始把UI Testing用起來!
  3. A Beginner’s Guide to Automated UI Testing in iOS

學習的過程中走了不少彎路,如果你看到下面這兩篇文章,建議立刻按下 Command + w,不要浪費時間去學習錯誤知識了:

  1. How Appium Works?
  2. Appium簡介及工作原理
  3. 用例項告訴你,如何利用Appium實現移動終端UI自動化測試

相關文章