干貨|app自動(dòng)化測(cè)試之Appium 源碼修改定制分析
Appium 是由 Node.js 來(lái)實(shí)現(xiàn)的 HTTP 服務(wù),它并不是一套全新的框架,而是將現(xiàn)有的優(yōu)秀的框架進(jìn)行了集成,在 Selenium WebDriver 協(xié)議(JsonWireProtocol/Restful web service)的基礎(chǔ)上增加了移動(dòng)端的支持,使 Appium 滿足多方面的需求。
官方提供更詳細(xì)的 Appium 結(jié)構(gòu)說(shuō)明:https://appium.io/docs/en/contributing-to-appium/appium-packages/
Appium 框架結(jié)構(gòu)
Appium 是由多個(gè)子項(xiàng)目構(gòu)成的,github 訪問(wèn)如下圖:

Appium 由 Appium 以及其它的工作引擎包括:appium-xcuitest-driver、appium-android-driver、appium-ios-driver、appium-uiautomator2-server、appium-base-driver 等組成。下載 Appium 這個(gè)項(xiàng)目進(jìn)行分析,發(fā)現(xiàn) Appium 有著非常復(fù)雜的目錄結(jié)構(gòu),如下圖:

其中重要的目錄如下:
項(xiàng)目中有個(gè)文件 package.json ,這個(gè)文件是項(xiàng)目的描述文件。對(duì)項(xiàng)目或者模塊包的描述,比如項(xiàng)目名稱,項(xiàng)目版本,項(xiàng)目執(zhí)行入口文件,項(xiàng)目貢獻(xiàn)者等等。npm install命令會(huì)根據(jù)這個(gè)文件下載所有依賴模塊,查看這個(gè)文件可以看到如下的信息:
"dependencies": {
?
?"@babel/runtime": "^7.6.0",
?
??"appium-android-driver": "^4.20.0",
? ?"appium-base-driver": "^5.0.0",
??
?"appium-espresso-driver": "^1.0.0",?
? ?"appium-fake-driver": "^0.x",
??
?"appium-flutter-driver": "^0",?
? ?"appium-ios-driver": "4.x",
??
?"appium-mac-driver": "1.x",?
?? ?"appium-support": "2.x",
??
?"appium-tizen-driver": "^1.1.1-beta.4",
?
??"appium-uiautomator2-driver": "^1.37.1",?
? ?"appium-windows-driver": "1.x",
?
??"appium-xcuitest-driver": "^3.0.0",
?
?...
?
},
dependencies 表示此模塊依賴的模塊和版本信息。從這里面可以看到它依賴很多 driver ,比如 appium-android-driver、appium-base-driver、appium-espresso-driver、appium-ios-driver、appium-uiautomator2-driver 等等。下面我們會(huì)根據(jù) appium-uiautomator2-driver 重點(diǎn)對(duì) Android 測(cè)試驅(qū)動(dòng)的源碼進(jìn)行分析。
appium-uiautomator2-server
appium-uiautomator2-server 是針對(duì) UiAutomator V2 提供的服務(wù),是一個(gè)運(yùn)行在設(shè)備上的 Netty 服務(wù)器,用來(lái)監(jiān)聽(tīng)指令并執(zhí)行 UiAutomator V2 命令。
早期版本 Appium 通過(guò) appium-android-bootstrap 實(shí)現(xiàn)與 UiAutomator V1 的交互,UiAutomator2 修復(fù)了 UiAutomator V1 中遇到的大多數(shù)問(wèn)題,最重要的是實(shí)現(xiàn)了與 Android 系統(tǒng)更新的分離。
Appium 底層執(zhí)行 Android 測(cè)試真正的工作引擎是一個(gè) JAVA 項(xiàng)目 appium-uiautomator2-server??梢詫⑦@個(gè)項(xiàng)目克隆到本地,使用 Android Studio 工具或者其它的 JAVA 項(xiàng)目 IDE 工具打開(kāi)這個(gè)項(xiàng)目。
appium-uiautomator2-server啟動(dòng)
從 README 文件可以看到啟動(dòng)服務(wù)的方式:
Starting server
push both src and test apks to the device \?
and execute the instrumentation tests.?
?adb shell am instrument -w \?
io.appium.uiautomator2.server.test/\
androidx.test.runner.AndroidJUnitRunner
找到 AppiumUiAutomator2Server.java 這個(gè)文件,如下圖:

startServer( ) 方法就是它的啟動(dòng)入口函數(shù)。這個(gè)函數(shù)里面調(diào)用了 ServerInstrumentation 類里面的 startServer( ) 方法。如下圖:

startServer( ) 方法會(huì)創(chuàng)建一個(gè)新的線程來(lái)處理一系列的邏輯。
AppiumServlet解析
AppiumServlet 是一個(gè)典型 HTTP 請(qǐng)求的處理協(xié)議。使用 AppiumServlet 來(lái)管理請(qǐng)求,并將 Driver 發(fā)過(guò)來(lái)的請(qǐng)求轉(zhuǎn)發(fā)給對(duì)應(yīng) RequestHandler,它會(huì)監(jiān)聽(tīng)下面的 URL
...?
register(postHandler, new FindElement(“/wd/hub/session/:sessionId/element”));
register(postHandler, new FindElements(“/wd/hub/session/:sessionId/elements”));
...
當(dāng)這些 URL 有請(qǐng)求過(guò)來(lái),AppiumServlet 會(huì)對(duì)它執(zhí)行相關(guān)的處理。比如查找元素、輸入、點(diǎn)擊等操作。以查找元素為例,實(shí)現(xiàn)類里可以看到下面一段代碼:
...
final String method = payload.getString("strategy");
final String selector = payload.getString("selector");
final String contextId = payload.getString("context");
...
通過(guò)這三個(gè)屬性“strategy”、“selector”、“context” 來(lái)定位元素。在 Appium 對(duì)應(yīng)的日志中可以看到這個(gè)操作。
2020-04-08 10:42:37:928 [HTTP] --> POST /wd/hub/session/f99fe38b-445b-45d2-bda0-79bf12e8910e/element
2020-04-08 10:42:37:929 [HTTP] {"using":"xpath",\
"value":"//*[@text=\"交易\"]"}
2020-04-08 10:42:37:930 [W3C (f99fe38b)] Calling \
AppiumDriver.findElement() with args: ["xpath","//*[@text=\"交易\"]","f99fe38b-445b-45d2-bda0-79bf12e8910e"]
...
2020-04-08 10:42:37:931 [WD Proxy] Matched '/element' to \
command name 'findElement'
2020-04-08 10:42:37:932 [WD Proxy] Proxying [POST /element] to \
[POST http://127.0.0.1:8200/wd/hub/session/\
0314d14d-b580-4098-a559-602559cd7277/element] \
with body: {"strategy":"xpath","selector":\
"//*[@text=\"交易\"]","context":"","multiple":false}
...
2020-04-08 10:42:39:518 [W3C (f99fe38b)] Responding \
to client with driver.findElement() \
result: {"element-6066-11e4-a52e-4f735466cecf":\
"c57c34b7-7665-4234-ac08-de11641c8f56",\
"ELEMENT":"c57c34b7-7665-4234-ac08-de11641c8f56"}
2020-04-08 10:42:39:519 [HTTP] <-- POST /wd/hub/session/f99fe38b-445b-45d2-bda0-79bf12e8910e/element 200 1590 ms - 137
上面代碼,定位元素的時(shí)候會(huì)發(fā)送一個(gè) POST 請(qǐng)求,Appium 會(huì)把請(qǐng)求轉(zhuǎn)為 UiAutomatorV2 的定位,然后轉(zhuǎn)發(fā)給 UiAutomatorV2。
擴(kuò)展功能
在 FindElement.java 中實(shí)現(xiàn)了 findElement( ) 方法,如下圖:
private Object findElement(By by) throws UiAutomator2Exception, UiObjectNotFoundException {
? ? ? ?refreshAccessibilityCache();
? ? ? ?if (by instanceof ById) {
? ? ? ? ? ?String locator = rewriteIdLocator((ById) by);
? ? ? ? ? ?return CustomUiDevice.getInstance().findObject(androidx.test.uiautomator.By.res(locator));
? ? ? ?} else if (by instanceof By.ByAccessibilityId) {
? ? ? ? ? ?return CustomUiDevice.getInstance().findObject(androidx.test.uiautomator.By.desc(by.getElementLocator()));
? ? ? ?} else if (by instanceof ByClass) {
? ? ? ? ? ?return CustomUiDevice.getInstance().findObject(androidx.test.uiautomator.By.clazz(by.getElementLocator()));
? ? ? ?} else if (by instanceof By.ByXPath) {
? ? ? ? ? ?final NodeInfoList matchedNodes = getXPathNodeMatch(by.getElementLocator(), null, false);
? ? ? ? ? ?if (matchedNodes.isEmpty()) {
? ? ? ? ? ? ? ?throw new ElementNotFoundException();
? ? ? ? ? ?}
? ? ? ? ? ?return CustomUiDevice.getInstance().findObject(matchedNodes);
? ? ? ?}
? ? ? ?...
? ?}
findElement( ) 方法具體的提供了 ById、ByAccessibilityId、ByClass、ByXpath 等方法,可以擴(kuò)展這部分功能,如果將來(lái)引申出來(lái)一些功能,比如想要通過(guò)圖片、AI 定位元素,可以在上面的 findElement( ) 方法里面添加 else if (by instanceof ByAI) 方法,來(lái)創(chuàng)建新類型ByAI并且增加功能的實(shí)現(xiàn)。比如未來(lái)新增了 AI 來(lái)定位元素的功能,可以使用 AI 的插件(基于 nodejs 封裝的一個(gè)插件)test.ai 插件(https://github.com/testdotai/appium-classifier-plugin)
用法:
driver.find_element('-custom', 'ai:cart');
項(xiàng)目構(gòu)建與apk安裝
完成代碼的修改之后需要重新編譯生成相應(yīng)的 apk 文件,并放到 Appium 對(duì)應(yīng)的目錄下。
Android Studio → 項(xiàng)目 Gradle → appium-uiautomator2-server-master → Task-other 下。
分別雙擊 assembleServerDebug 與 assembleServerDebugAndroidTest 即可完成編譯,編譯完成會(huì)在目錄下生成對(duì)應(yīng)的兩個(gè) apk 文件。
assembleServerDebugAndroidTest.apk
構(gòu)建后 apk 所在目錄:app/build/outputs/apk/androidTest/server/debug/appium-uiautomator2-server-debug-androidTest.apk 這個(gè) apk 是個(gè)驅(qū)動(dòng)模塊,負(fù)責(zé)創(chuàng)建會(huì)話,安裝 UiAutomator2-server.apk 到設(shè)備上,開(kāi)啟 Netty 服務(wù)。
assembleServerDebug
構(gòu)建后 apk 所在目錄:app/build/outputs/apk/server/debug/appium-uiautomator2-server-v4.5.5.apk,這是服務(wù)器模塊,當(dāng)驅(qū)動(dòng)模塊初始化完畢,服務(wù)器就會(huì)監(jiān)聽(tīng) PC 端 Appium 發(fā)送過(guò)來(lái)的請(qǐng)求,將請(qǐng)求發(fā)送給真正底層的 UiAutomator2。
另外,也可以使用命令來(lái)進(jìn)行構(gòu)建:
gradle clean assembleE2ETestDebug assembleE2ETestDebugAndroidTest
將編譯完成的 APK,覆蓋 Appium 目錄下對(duì)應(yīng)的 APK 文件。需要先使用命令查找 Appium 安裝目錄下的 Uiautomator server 對(duì)應(yīng)的 APK,MacOS 操作命令如下:
find /usr/local/lib/node_modules/appium -name "*uiautomator*.apk"
使用上面的命令會(huì)發(fā)現(xiàn)關(guān)于 Uiautomator 的兩個(gè) apk 文件,如下:
$ find /usr/local/lib/node_modules/appium -name \?
"*uiautomator*.apk"
/usr/local/lib/node_modules/appium/node_modules\
/appium-uiautomator2-server/apks/\?
appium-uiautomator2-server-v4.5.5.apk
/usr/local/lib/node_modules/appium/\?
node_modules/appium-uiautomator2-server\?
/apks/appium-uiautomator2-server-debug-androidTest.apk
將編譯好的 APK 替換這個(gè)目錄下的 APK 即可。
客戶端會(huì)傳遞 Desired Capabilities 給 Appium Server 創(chuàng)建一個(gè)會(huì)話,Appium Server 會(huì)調(diào)用 appium-uiautomator2-driver 同時(shí)將 UiAutomator2 Server 的兩個(gè) apk 安裝到測(cè)試設(shè)備上(也就是上面生成的兩個(gè) apk 文件)。