基於 Python3 的網頁自動化測試框架實現_什麼是POM《四》

今天又是加班的一天,窗外的天空好美。——《丫子》
在這裏插入圖片描述

前言

在正式開始封裝 selenium 之前,我們首先要搞清楚如下幾個問題:

  • 到底什麼是POM,它比不用 POM好在哪裏?
  • OOP在POM中到底扮演什麼樣的角色?它爲POM提供了哪些特性?
  • 如果使用POM,該怎麼進行規劃?怎麼去封裝Selenium?

到底什麼是POM,它比不用POM好在哪裏?

對於接觸過自動化測試的同學來說,大概都能說出些自己的理解。這裏說一下我的理解,我打算用一個代碼例子來說明這個問題,比圖和文字應該更直觀一些。

這次我們打算用 https://www.utest.com/ 這個網站來寫測試用例,進入首頁後,先打開左側導航欄,
在這裏插入圖片描述
依次點擊左側導航欄上部的5個按鈕,

在這裏插入圖片描述
然後驗證其進入相應的正確頁面。
在這裏插入圖片描述
假設我們根本不知道POM是什麼東西,也不知道任何與測試框架有關的知識,那我們應該怎麼寫代碼來達到測試需求呢?
我首先想到的就是按部就班地把測試步驟一步一步地實現(還在糾結於怎麼定位元素,或者不知道selenium API的同學,趕緊去補習吧~),在根目錄下新建Samples文件夾,並新建NoPom.py,代碼如下:
在這裏插入圖片描述
在這裏插入圖片描述
而這裏是Log日誌:
在這裏插入圖片描述
稍微說明一下,由於反覆寫5遍 驗證導航欄 -> 點擊導航欄按鈕 -> 驗證title標籤文本 實在是太麻煩,所以我在測試腳本中將導航欄按鈕和title標籤文本拼成了字典,並用for循環遍歷了這個字典,從而減少了代碼量。但是儘管如此,這個測試腳本依然有如下問題存在:

  • 定位元素和測試步驟寫在同一個測試腳本里,耦合太高。一旦元素定位發生變化,那麼需要修改所有測試腳本里的元素定位方式。
  • 元素是在測試腳本里定位的,無法複用。如果該元素在多個測試腳本里都需要用到,那麼需要在每個測試腳本中去定位元素,代碼大量冗餘。
  • 使用強制等待 time.sleep() 方法缺少彈性,而且穩定性也不足。在性能高的機器上會浪費測試時間,在性能低的機器上可能等待時間又不足,會出現無法定位的情況。這對於一個長期反覆運行的測試腳本來說,不可取。
  • 缺少測試用例組織方式,如果驗證5個跳轉頁面的title標籤文本是5條用例的話,則一個腳本無法控制精確執行哪一條測試用例。如果將這一條腳本拆分成5條腳本的話,又會因爲出現前2點的問題。
  • 測試結果不明顯。目前的測試結果只有 log 文件,若想查看是否驗證失敗只能去 log 文件中進行字符串匹配才能發現是否有驗證失敗。效率低下而且非常不方便。
  • 測試腳本過長,可讀性比較差,不利於後期維護和擴展。
    針對以上幾點問題我們來設想解決方案:
  • 先說第三點,這個其實可以通過調用 selenium 的顯式等待 WebDriverWait方法,再結合 until 和 until_not 方法來處理,這個稍後再說。
  • 第四點,需要一個組織測試用例的工具,python3自帶的unittest庫就可以解決這個問題,當然也有一些其他的測試框架工具,比如 pytest,RobotFramework等,本框架打算使用最簡單的unittest,基本能實現常規的測試需求,具體的實現方式稍後再說。
  • 第五點,需要一個生成報告的工具,unittest有一個擴展文件可以解決這個問題,同樣,我們稍後再說。
  • 最後看第一和第二點。這兩點其實算是同一個問題。若想解決這個問題,我們自然而然就想到了對元素定位進行封裝,存放在另一個文件中,對其統一進行管理。然後在測試腳本中通過導包的方式導入這些元素。這樣就可以做到一處定義,多處使用,減少了冗餘代碼。同時由於元素的定義是在一個獨立文件中進行的,測試腳本只負責調用,所以也自然做到了解耦,一旦元素定位發生改變,只需要更改定義處的代碼即可,測試腳本處完全不需要改動。

到此爲止,大概能回答第一個問題“到底什麼是POM,它比不用 POM好在哪裏?”一半的問題了。我們知道了最傻的方式有哪些缺點,但是到底什麼是POM呢?POM和基本的封裝之間還有什麼區別?這裏我就不寫代碼來舉例了,我們回頭去看羣主博客上的例子
點擊跳轉 ,羣主是如何把最基本的定位元素,操作元素按POM模式來封裝的呢?
在這裏插入圖片描述
在這裏插入圖片描述

  • 首先定義一個通用的find_element(self, selector) 方法
    在這裏插入圖片描述
  • 然後把元素的定位方式按照固定格式寫在一個頁面類裏
    在這裏插入圖片描述
  • 然後再封裝selenium的click()方法,其中該方法調用了之前定義的 find_element() 方法在這裏插入圖片描述
  • 最後看看是怎麼把前三步串起來的
    在這裏插入圖片描述
    我們不一定需要完全仿照羣主的實現方式,但是需要讀懂這麼 實現的目的:主要是看第二步,普通封裝方式會將頁面元素的定位方式全存放在同一個文件裏,而羣主則是將其存放在相應的頁面類裏,後者就是POM。從代碼的實現來看,POM確實要比我之前寫的傻方式要簡潔得多,可讀性要高得多。至於POM比起普通封裝方式來說要好在哪裏呢,那就是第二個我們需要關注的問題。

OOP在POM中到底扮演什麼樣的角色?它爲POM提供了哪些特性?

上文說到了POM和普通封裝的區別,老規矩,我們直接上才藝:
在這裏插入圖片描述
搞錯了,上代碼
在這裏插入圖片描述
大家看到這裏有沒有一種恍然大悟的感覺?沒錯, 如果使用函數的形式來定義元素操作方式,發現沒有辦法傳遞driver這個變量!那麼selenium的方法也就沒辦法調用。

可能有些同學會說,那我可以先在其他文件中生成 driver 實例,然後再在這個文件中 import 進來,一樣可以用。但是這樣會有一個很嚴重的問題——一旦driver實例化之後,再在函數中調用 driver.click() 其實就是顯式調用了 click() 這個方法,driver會控制瀏覽器去當前頁面嘗試點擊元素,而這裏只是定義函數的地方,還沒到要具體操作元素的測試腳本的範圍,自然會拋出找不到元素的異常。

所以想要以函數的形式來定義,很有難度,我不知道有沒有妥善的解決方案。但是這並不是個無法攻克的難題——只要你以類方法的形式來定義就可以了。

這就牽扯到OOP的一個重要特性——在代碼運行時保持類中屬性的狀態。在定義類方法的時候,self.driver = driver這一步只是一個傳遞參數的過程,並沒有顯式的實例化 driver。而且程序一直記住了這個狀態,等到真正需要調用的時候,已經實例化後的driver 纔會傳遞給類進行頁面類的初始化並調用相應的方法,類中的屬性 self.driver 接收到實例化後的driver發生改變。

說了這麼一大堆可能有點繞,能理解是最好,如果理解不了可以自己動手嘗試一下。

除了保持類屬性的狀態,OOP的特性還有額外的好處,比如繼承。繼承是OOP最顯著的特性,我就不詳述了。我想說一下繼承在POM中是怎麼應用的。

第一點是定義一個基類class BasePage,然後把一些selenium的底層方法都封裝在BasePage裏,這樣所有BasePage的子類都可以繼承這些方法,不會有代碼冗餘出現。
第二點則偏向於業務,詳細分析需要測試的網站,將最外層最頂層最大最通用的頁面(一般都是html中比較靠上層級的元素)定義爲層級較高的頁面類,而一些分頁,子頁面,甚至頁面中的大型控件(比如超複雜的Grid)定義爲層級較低的頁面類,低層級頁面類去繼承高層級頁面類。這樣做的好處就是可以去掉重複定義頁面元素的冗餘。
我們舉個例子來說:
在這裏插入圖片描述
還是這個網站,左側導航欄,很明顯大家都能看出來這是網站的重要組成部分,絕大部分子頁面都可以隨時展開導航欄並對其進行操作。包括展開導航欄的展開按鈕,也是絕大部分子頁面都有的。所以我會在 直接繼承BasePage類的最頂層的HomePage類定義導航欄和展開按鈕,那麼繼承HomePage的所有子類也都會繼承導航欄和展開按鈕的元素定義和操作方法。

而如果某個頁面,發現展開按鈕的html改了,和其他頁面不一致(有可能是故意設計成這樣的),那麼不用更改父類HomePage中的展開按鈕定義方式,只需要在該頁面類中重寫展開按鈕的定義方式就可以了。這就是OOP的另一個重要特性——多態。

利用多態這種特性,我們可以拓展並定製父類中的方法——父頁面類定義通用的頁面元素,子頁面類根據各自的情況決定是繼承,還是定製,還是在父類基礎上擴展。Python3的OOP語法都支持相應的操作。

當然我在自己經歷過的自動化測試項目中還意識到一個 OOP特性帶來的額外好處——類本身擴展了命名空間。可能有些同學還沒意識到這個問題,比如說一個巨大的網站,裏面有N多個子頁面,每個頁面又有N多個元素(我之前經歷過一個項目,頁面元素寫了1300多個),如何去爲這些元素命名其實有時候是個很頭疼的問題。比如說我那個項目裏光是ok_button就有30多個。有些同學會說,這沒問題啊,按頁面層級來命名好了,比如homepage_vmgrid_vmnames_edit_textbox,這是個解決方案,但是說實話我見過比這個還長的變量名,當你的測試腳本中充斥着這種變量名的時候,整個人的心情都非常複雜 -_-,做代碼維護的同學的熱情也會降低不少。

而使用類,則可以減少這樣的變量名的長度,因爲不同的類中可以定義相同的變量名,就比如 homepage.okbutton 和 loginpage.okbutton ,雖然它們在定義時的名字都叫 okbutton,但在調用的時候因爲在前面加上了實例調用,所以即使出現在同一個python 文件中也絲毫沒有問題。

如果使用POM,該怎麼進行規劃?怎麼去封裝Selenium?

到這裏爲止,我們會發現POM這種模式實在是很巧妙。既然我們已經瞭解了它的優點,掌握了它的思想,那我們該如何去規劃自己框架的模式呢?下面我會介紹我自己的理解和所採用的方式。首先要根據業務中的測試經驗提取出常用的selenium AP,然後分析這些API的入參和返回值,甚至源碼,使用相應的技術來進行封裝。

我總結了一下,我之前 常用的API包括以下這些部分:

  • Webdriver類:selenium/webdriver/remote/webdriver,所有相應的瀏覽器Webdriver類都繼承自該類,該類封裝了很多控制瀏覽器的方法。我們測試腳本中生成的driver對象就是該類的實例。
  • WebElement 類:selenium/webdriver/remote/webelement,該類封裝了很多操作網頁元素的方法。element是Webdriver.find_element() 方法的返回值,同時又是WebElement類的實例對象,可以直接調用該類中的方法。我雖然看了源碼,但是到現在還沒搞懂,selenium是怎麼做到這一步的。如果有同學知道,請不吝賜教。
  • WebDriverWait 類:selenium/webdriver/support/wait,該類就是著名的顯式等待,結合類中的 until() 和 until_not() 方法可以智能地去判斷頁面元素。
  • 期望場景類:selenium/webdriver/support/expected_conditions,該類定義了各種各樣的期望場景,是結合WebDriverWait的until() 和 until_not() 的利器。
  • By類:selenium/webdriver/common/by,該類映射了8大元素定位方式所需的方式。如果需要將8大元素定位方法整合成一個定位方法,該類是必不可少的。
  • Alert類:selenium/webdriver/common/alert,該類封裝了操作網頁alert的方法。Webdriver.switch_to.alert() 的返回值,同時又是Alert類的實例對象,可以調用Alert的方法,這一點有點像WebElement類的方式。
  • ActionChains類:selenium/webdriver/common/action_chains,該類封裝了比較底層的交互方法,比如鼠標事件,鍵盤事件等。該類的構造函數中接受driver參數,所以該類的方法的使用方式和 Webdriver 類中的方法類似,只不過類名不同而已。
  • WebDriverException類:selenium/common/exception,該類繼承自Exception類並拓展出不少子類,對應整個selenium工具的異常情況。如果我們需要在封裝過程中捕獲特定的異常來做處理,那麼必須精確捕獲某一種異常。

再結合自己遇到的在自動化測試中的應用場景,我理想中的測試腳本寫起來應該包含以下幾種常用的格式:

element.operation(*args, **kwargs) – 操作頁面元素
page.operation(*args, **kwargs) – 操作瀏覽器
page.wait.something(*args, **kwargs) – 等待某些特定事件發生或不發生
page.action.something(*args, **kwargs) – 鼠標事件/鍵盤事件

我決定分爲幾部分來分別封裝selenium的這些API。

  • Browser類:該類負責page.operation(*args, **kwargs)部分,主要封裝Webdriver類中的方法,並且定義統一的私有方法_find_element() 。屆時BasePage類直接繼承Browser類,也繼承其中的方法。
  • Element 類:該類負責element.operation(*args, **kwargs)部分,主要封裝WebElement類中的方法,並且需要嵌入定位元素,智能等待過程。屆時測試腳本中直接調用Element的 對象.方法() 正好滿足 element.operation()的形式。
  • Wait 類:該類負責page.wait.something(*args, **kwargs)部分,主要封裝WebDriverWait 和expceted_conditions。由於不想每個頁面都去實例化 Wait,所以利用委託技術將Wait的對象傳遞給page的屬性 wait。
  • Action類:該類負責 page.action.something(*args, **kwargs)部分,主要封裝ActionChains。由於不想每個頁面都去實例化 Action,所以利用委託技術將Action的對象傳遞給page的屬性 action。

到這裏爲止,基本上確定了封裝selenium的方式,下面我們就正式開始寫代碼。

End

歡迎關注公衆號以及加羣討論,所有文章都會同步到公衆號,方便大家在碎片時間閱讀。
在這裏插入圖片描述
▲掃描二維碼“識別”關注 簡介:熱愛生活,享受旋律!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章