Appium With UiAutomator 2.0 運行 Multiple Session

「Appium With UiAutomator 2.0 實際運用及原理介紹」 我們了解了如何在 Appium 搭載 UiAutomator 2.0 執行自動化測試,但是若我們今天想要讓測試時程再次縮短,我們會需要使用到多裝置且運行 Multiple Session,但是根據我們實際運行 Multiple Session 後卻發現了一個嚴重性的問題,那就是 Appium UiAutomator 2.0 Proxy Server Port 會出現 「互搶互讓」 的狀態。

為什麼會有此狀況發生

回顧 「UiAutomator 1.0 與 UiAutomator 2.0 差異以及 Appium 實作」 提到的差異重點,因為 appium-uiautomator2-server 是透過 Netty Server 來處理 Client Host 與 Devices/Emulators 之間的溝通,因此相較於 appium-android-bootstrap 只透過單一 Server Socket 在運作,實質上 appium-uiautomator2-server 是多了一層媒介橋樑,以下我們將 Single SessionMultiple Session 獨立出來觀察:

● Single Session

Single Session 的自動化下,使用者不會有感覺到異狀,因為 appium-uiautomator2-server 本身會給予 Session 一個預設的 Proxy Server Port,如下方範例所示,此範例 Client 發出 request 時的 desired capabilities 僅給予參數 automationName 並 Assign 為 uiautomator2,沒有額外再多給任何參數,此狀況下 Appium 也不會遇到異狀,因為 Client 只有發出唯一一個 Session Request,而 appium-uiautomator2-server 本身就會給予 Session 預設 Proxy Server Port 8200,因此可以發現 當 Appium 在 Single Session 下只需要給予 UiAutomator 2.0 的 desired capabilities 即可正常運作

[UiAutomator2] Waiting up to 20000ms for UiAutomator2 to be online...
[JSONWP Proxy] Proxying [GET /status] to [GET http://localhost:8200/wd/hub/status] with no body
[JSONWP Proxy] Proxying [GET /status] to [GET http://localhost:8200/wd/hub/status] with no body
[JSONWP Proxy] Proxying [GET /status] to [GET http://localhost:8200/wd/hub/status] with no body
[JSONWP Proxy] Got response with status 200: "{\"sessionId\":\"SESSIONID\",\"status\":0,\"value\":\"Status Invoked\"}"

● Multiple Session

Multiple Session 的自動化下,一般在 Setup 時會分別給予不同裝置不一樣的 Appium Remote Port,若直接啟動自動化測試的話,會因為我們沒有同時分別給予不同裝置不一樣的 Proxy Server Port,而導致各個裝置的 Netty Server 使用「同一個」 Proxy Server Port ,但是對於各個裝置而言,他們並不知道互相之間使用到同一 Port(各個裝置都沒指定,因此各個裝置都會走預設 Port 8200 ),如此一來就會造成各個裝置的所發出的 Requests 跟接收到的 Actions 銜接不上而造成測試錯亂,常見的錯誤訊息如下,可以發現 Proxy Server Port 8200 同時接收到多個 Actions 或是在不對的時機被關閉。

Original error: Could not proxy command to remote server.
Original error: Error: connect ECONNREFUSED 127.0.0.1:8200
---------------------------------------------------------------------------------------
Original error: java.lang.IllegalThreadStateException: Thread already started
at java.lang.Thread.checkNotStarted(Thread.java:849)
    at java.lang.Thread.start(Thread.java:1059)
    at io.appium.uiautomator2.model.NotificationListener.start(NotificationListener.java:32)
    at io.appium.uiautomator2.model.Session.<init>(Session.java:23)
    at io.appium.uiautomator2.model.AppiumUiAutomatorDriver.initializeSession(AppiumUiAutomatorDriver.java:19)
    at io.appium.uiautomator2.handler.NewSession.safeHandle(NewSession.java:31)
    at io.appium.uiautomator2.handler.request.SafeRequestHandler.handle(SafeRequestHandler.java:56)
    at io.appium.uiautomator2.server.AppiumServlet.handleRequest(AppiumServlet.java:202)
    at io.appium.uiautomator2.server.AppiumServlet.handleHttpRequest(AppiumServlet.java:193)
    at io.appium.uiautomator2.http.ServerHandler.channelRead(ServerHandler.java:44)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(Abstra...
    [ Message content over the limit has been removed. ]
...nelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:366)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:352)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:911)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:131)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:611)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysPlain(NioEventLoop.java:514)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:468)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:438)
    at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:140)
    at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:144)
    at java.lang.Thread.run(Thread.java:818)

P.S. 若有興趣觀察「互搶互讓」的狀況,也能給不同 Session 的不同裝置給予「相同」的 Proxy Server Port,如此一來即可觀察到:「Device 1 即將要做的動作會在 Device 2 上出現」、「Device 1 測試後要關閉 Session 時卻把 Device 2 Session 關閉造成 Device 2 找不到 Session」等等的有趣現象。

如何克服 Multiple Session 的錯誤狀況

其實解決方法很簡單,就如同在 desired capabilities 給予 automationName 一樣,這邊需要額外給予一個參數為 systemPort ,而給的值即為 Client 要分配給各個裝置的 Proxy Server Port Number ,如下範例所示,如此一來即能解決各個裝置誤用到同個 Proxy Server Port 的問題了。

[GitHub] Official Pull Requests

● Device 1

def setUp(self):
    desired_caps = {}
    desired_caps['platformName'] = 'Android'
    desired_caps['platformVersion'] = '7.0'
    desired_caps['deviceName'] = 'Android Emulator Device 1'
    desired_caps['app'] = '/.../.../testing.apk'
    desired_caps['automationName'] = 'uiautomator2'
    desired_caps['systemPort'] = '8200'           ### Other Devices Shouldn'd be 8200.

    self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

● Device 2

def setUp(self):
    desired_caps = {}
    desired_caps['platformName'] = 'Android'
    desired_caps['platformVersion'] = '6.0'
    desired_caps['deviceName'] = 'Android Emulator Device 2'
    desired_caps['app'] = '/.../.../testing.apk'
    desired_caps['automationName'] = 'uiautomator2'
    desired_caps['systemPort'] = '8204'           ### Other Devices Shouldn'd be 8204.

    self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)

結言

經過前兩篇文章以及此篇文章的一番著墨,我們實際上會發現其實要運作讓 UiAutomator 2.0 框架運作在自動化測試上並不難,相對於複雜的測試框架,Appium 提供了完善的 Setup 參數設定,如果就運作 Single Session 的自動化測試而言,我們僅需要在 Client 發出 Request 時在 desired capabilities 給予 automationName 即可順利運作 UiAutomator 2.0,這對於一般使用者在 Local 端已經足以負荷大多數的自動化測試了,而且若原先是使用 UiAutomator 1.0 框架那又會是一大有感的升級。

對於一些複雜性高的 Application 來說,Multiple DevicesMultithreading 對於相對應的測試團隊就會是個很重要的因素,但是通常在從 UiAutomator 1.0 升級到 UiAutomator 2.0 的過程中,往往我們會忽略掉框架背後的運作架構,進而導致測試 Setup 上的毀滅性錯誤,像此篇文章整體在討論的 How To Multiple Session ,其實只需要在 desired capabilities 額外給予 Proxy Server Port 即可,設定上的成本近乎跟設定 automationName 一樣少,過程中我們卻容易踏入誤以為「測試框架尚未穩定」的路途而導致 Debug 心態轉為消極,但實質上我們錯過的可能會是個穩定又快速測試框架,稍微關注一下背後的運作原理,或許也能獲得不少的經驗,以此紀錄整體升級過程的心路歷程。

Multithreading 的必要處:根據我們實際的經驗來說明,平均一條 2 分鐘的測試假設採用序列式跑完,62 條測試則需要 120 分鐘左右,但是若我們妥善分配 Test Suite 且向下分配給三台裝置跑,並且搭配上 UiAutomator 2.0 框架,實際運作下來我們可以在 33 分鐘左右即完成 62 條測試,這對於自動化測試時程會是個很大幅度的縮減。

參考資料

發表留言