1. 程式人生 > >UI 自動化常用設計模式 (二)

UI 自動化常用設計模式 (二)

作者:孫高飛

 

狀態模式

狀態模式之所以常用是因為在我們的很多業務邏輯中都會有不同狀態的出現,比如訂單的狀態,任務的狀態。而不同的狀態下UI上會有不同的行為。 比如不同的控制元件的展示, 不同的報錯資訊等。 我們往往需要驗證不同狀態下的邏輯。 但是我們的狀態往往比較多(一般怎麼都會有個5,6種吧)。 所以我們需要一種合適的方法來組織和管理這些狀態下的行為。
舉個例子, 在我們的產品中,每一個運算元都有:未配置,配置成功,等待執行,執行中,執行成功,執行失敗和終止這6種狀態。運算元在每種狀態下顯示的控制元件和能操作的邏輯是不一樣的。我們一個最簡單的需求就是,在case中驗證每一種狀態下,UI控制元件的展示是符合需求的。 比如處於未配置狀態的運算元是不能執行和停止的, 執行中的運算元是可以看見停止按鈕但是無法顯示執行按鈕,相反的配置完成的運算元是可以顯示執行按鈕但是不能展示停止按鈕的。 

 

上面是我們的狀態抽象類的一部分程式碼截圖。 裡面看到有一個抽象方法是validateNodeUI, 用來執行驗證操作。 不同狀態的子類有著不同的邏輯。 比如下面這個處於Running狀態的子類。

 

這個running狀態的子類覆蓋實現了父類的validateNodeUI方法,running狀態的運算元只能看到停止按鈕。 然後我們再看看終止狀態的運算元和執行成功狀態的運算元。

 

 

終於狀態的運算元是可以重新執行的但是看不到停止按鈕, 而執行成功的運算元因為已經到了運算元的最終狀態, 所以它既不能執行,也不能停止。 這樣我們就有了我們的狀態類。 接下來我們看怎麼使用這些狀態類。 我們需要在所有運算元的父類(Node)裡寫一個查詢當前狀態的方法。意思是通過UI來檢視當前運算元的執行狀態是哪一種並返回。然後在自己驗證控制元件的方法中,使用相應的狀態類。如下:

 

PS: 也許會有小夥伴問上面寫了那麼多if else來建立各種不同的狀態類, 為什麼不用工廠模式來做? 那是因為整個專案中只有這一個地方使用了狀態類, 也就沒有必要專門封裝一個工廠類了。 大家要小心過度設計哦~~

這樣我們的每一種狀態下的UI控制元件的驗證就都寫好了。 case中使用的時候入下:

 

當然狀態模式中不只有驗證UI控制元件這一個功能。 由於不同的狀態下擁有著不同的行為,假如由於case編寫者的失誤, 非要在終止狀態下的運算元上點選終止按鈕, 那肯定會在查詢控制元件超時後丟擲一個element not found的error出來。 這樣有兩個不好的地方:

  • element not found 的報錯資訊並不友好,尤其是有些控制元件的查詢方式用xpath查詢的,用非文案的方式查詢的。 讓會再看report的時候並不能很容易看出來錯誤出在了哪裡。需要到程式碼裡去看或者debug。
  • 一般查詢控制元件的API都是自旋等待並設定超時時間的,比如我再專案中設定的隱式等待時間是10s. 要等10s後才丟擲這個異常也是滿耗時的。我們希望立刻就丟擲這個錯誤。

所以不同狀態的子類中可以去實現不同的行為, 如下:

 

可以看到停止狀態的子類的stop方法會直接丟擲一個異常。 只要一個物件的行為取決於它的狀態,並且它必須在執行時刻根據狀態改變它的行為,就可以使用狀態模式

接下來分析一下狀態模式的優點:

  • 在產品複雜業務邏輯和狀態流轉下, 可以有效的以一種結構化的方式把我們的程式碼組織起來。 如果我們不使用狀態模式,會導致在case或者page類中出現大量的if else。導致後期的維護成本和可讀性都很差。

裝飾器模式

裝飾器,介面卡和代理我覺得可以不用分得那麼清, 都是為了使現有的類的行為滿足我們新的需求,而做的一層封裝。 最經典的例子就是java io中的高階流,低階流了。 感興趣的同學可以去看看。 那麼在UI自動化中會有什麼情況會用到呢? 最常用的就是重試的功能。 UI自動化是出了名的不穩定的, 有很多公司都會啟用失敗重跑的功能。 記得我再外包那些年的時候, 經歷過的國外公司幾乎都會在UI自動化中實現失敗重跑的功能。邏輯很暴力,當case執行失敗的時候就重跑整個case。 這種暴力的做法優缺點很分明。
優點:

  • 實現簡單,一些測試框架比如testng已經支援這種功能 缺點:
  • 失敗的case也會進入report中,要對report單獨處理
  • 暴力的不管三七二十一,只要失敗就重跑的策略會在很多時候大大的增加了測試的執行時間。 比如本來就是bug引起的失敗還是會去重新執行的話,最終還是會失敗的,白白的浪費了執行的時間和資源。尤其是在像我們這種有很多長時間的非同步任務的產品,這種策略更加無法忍受。

鑑於上面說的缺點, 我們希望可以有重試的功能, 但是還比較希望能夠控制重試的執行粒度。比如執行時間短的,成本較低的,容易出錯的,UI操作複雜的是可以重試的, 但是那些執行時間長的,不容易出錯的非UI任務我們是不重跑的。 比如對於我們的產品來說,在UI上設定運算元的配置,元件運算元的dag圖,這些都是UI操作,執行時間較短,但是大量的UI操作是比較容易出錯的。 但是這些運算元一旦執行起來,就都是後臺的操作,UI上沒有任何變化,這時候case就是在那裡自旋等待,輪詢運算元狀態而已,一般來說,只要運算元執行失敗了,那基本就是真失敗了,就算再跑一次也大概率還是會失敗的,即便不是bug,那不管是因為環境問題還是叢集問題,都不是一個失敗重試能解決的。 所以我們要重試的就是組裝Dag的操作。 還記得上一次說建造者模式中的DAGBuilder麼? 是的,我們現在就是要對它進行失敗重試,dagBuilder只負責構建UI上的複雜操作,並不負責執行和等待後臺的任務結束,正是把構建和執行拆分了開來,正合適進行失敗重跑的場景(這裡也體現出設計原則中的一個類只負責一件事的好處)。 那麼問題來了, 原本的DagBuilder就只是一個在UI上構建DAG圖的操作,並沒有失敗重跑的操作。 而我們也並不希望把失敗重跑的功能加到DAGBuilder裡面, 一來是因為我們要遵循設計原則,只讓一個類負責儘量少的事情。 二來是有些情況下,我們也希望沒有失敗重跑的功能,直接將異常丟擲來。由呼叫方處理。 所以我們使用裝飾器模式, 封裝一個裝飾器類。 如下:

 

  • 裝飾器類可以實現被裝飾的DagBuilder的介面,保持介面相容和使用方式一致
  • 裝飾器類在建立時傳遞被裝飾的物件,然後在方法中呼叫被裝飾的DagBuilder的方法。並新增自己的新功能, 也就是失敗重跑。

上面截圖中,就是在dagBuilder丟擲異常後,捕獲異常,然後重置頁面初始狀態(重新整理頁面)重新呼叫DagBuilder的方法。 使用的時候入下:

 

PS: 這裡發現我們只對dagBuilder做了失敗重試, 大家會發現上面的登入,頁面切換,建立project等操作並沒有重試的功能。 因為這些操作簡單,並且足夠穩定, 一旦失敗,除開bug的原因就是環境發生了問題或者是UI發生了變化而指令碼沒有及時更新。不論哪種情況都不是重試能解決的, 當然也有一種情況是環境的服務出現了一些效能問題, 比如我們曾經遇見過叢集IO負載過高,導致一個介面請求就數秒甚至10幾秒。 所以有時這些穩定簡單的操作會超時,這時候重試是有可能會讓這些操作跑過去,但是我們是不會這麼做的。 因為環境本身就出現了問題,這裡我們是希望case就這麼直接失敗的,減少不必要的執行時間。所謂失敗了也要快速的失敗,快速的反饋。

PS2:使用python的同學實現失敗重試就簡單多了, python自帶裝飾器的語法糖。

原型模式

原型模式是一個很簡單的模式,它適用於我們要複製一個物件的時候。 那在UI自動化中,有什麼場景需要我們複製一個物件呢。 以我們產品為例,在執行測試的時候,一個DAG中會出現兩個相同的運算元, 比如一般會有兩個特徵抽取運算元,一個連線訓練資料,一個連線測試資料。 但他們兩個的配置是相同的(在機器學習中,如果這倆哥們不一樣,就出問題了)。 那麼問題來了, 我們看要設定一個特徵抽取運算元都需要哪些引數。

 

這樣就很煩了,我要手動建立兩個FENode的物件,把完全相同的引數set進去。也許有小夥伴會說你可以就用一個FENode作為引數,重複利用麼。 這也是不行的, 雖然他們的配置相同,但是有一樣是不同的。 那就是在UI上搜尋控制元件的方式。 由於這是兩個完全一樣的運算元,他們擁有相同的文案,相同的控制元件。唯一能區分他們的方式就是在DOM樹中他們的下標[index]。 所以在每個Node裡都會有一個額外的屬性叫index,表明他們在UI上是第幾個同類運算元。 如下:

 

所以如果我們重複使用一個FENode,你會發現你操作的還是同一個FE。 所以這時候我們希望能有一個clone方法, 能夠幫我們創造出一個新的物件的同時,還擁有原始物件中一樣的屬性。 這在java中比較容易實現。 在java中object有clone方法,而所有物件都是整合object的。 所以我們只需要實現一個名字叫Cloneable的空介面,標記本類是可以clone的,就可以直接呼叫object的clone來完成複製物件的目的了。 如下:

 

 

看上面我們直接呼叫了object的clone來複制物件, 然後讓index屬性自增1。這樣就滿足了我們的需要。 

原型模式在UI自動化中常見的場景都是類似這種,我們要在UI上做很多相似的UI操作, 這些操作需要傳遞很多配置。 這些配置大多數是相同的,但是有一小部分是不同的。而我們又不能直接通過不停的改變一個物件的屬性來完成這項任務(因為之後還要使用這些物件做其他操作)。 所以需要原型模式出馬。比如我們要在專案中匯入很多資料。 這些資料的匯入方式是差不多的,比如格式,資料來源等等, 可能只有資料的路徑和名字不一樣。 當然我們也可以只使用一個物件,引入一個數據後,立馬改變這個物件的資料路徑和名字,去引入下一個物件。 這樣做也是可以的,但是這樣做的壞處是你之後就不能使用這個物件操作之前的那些資料了。 比如我們引入資料後需要等待資料引入結束, 但是你的當前物件的名字和都變成最後一次操作的配置了。 你已經失去了跟蹤之前的資料匯入的能力了。 所以這時候原型模式就很有用了, 迅速為你clone出一個符合你需求的物件使用。

PS:上面講的使用object的clone的方式都是淺拷貝, 什麼是淺拷貝呢? 比如我們物件中的屬性如果有引用型別,例如list,map或者另一個物件。 這時候是不會複製一個新的,而是直接把這些引用型別屬性的引用地址複製過來。也就是說,雖然外層物件已經是新的了,但是裡面的引用屬性使用的還是一個物件。 而如果是深拷貝的話,它是會把引入型別也clone一份出來。 當然如果要實現深拷貝,那就需要我們自己編寫邏輯了。 但是大多數情況下淺拷貝是可以滿足我們的需求的。 例如上面的關於特徵抽取運算元的例子,不一樣地方只是一個int型別的index。 所以這時候淺拷貝完全夠用。