『心善淵』Selenium3.0基礎 — 30、UI自動化測試之POM設計模式

繁華似錦Fighting發表於2021-07-16

(一)POM模式介紹

1、什麼是POM介紹

POM是Page Object Model頁面物件模型的簡稱。

POM是為Web UI元素建立Object Repository的設計模式 。

在這個模型下,對於應用程式中的每個網頁,應該有相應的頁面類。

Page類將會找到該Web頁面的WebElements,並且還包含對這些WebElements執行操作的頁面方法。

POM設計模式旨在為每個待測試的頁面建立一個頁面物件,將那些繁瑣的定位操作封裝到這個頁面物件中,只對外提供必要的操作介面,是一種封裝思想。

白話總結:

我們所做的自動化測試,就是模擬人在瀏覽器上的操作。而自動化測試中操作所有的元素的步驟,無非就是先定位到頁面的各種元素,然後在模擬各種對元素執行的操作。

而我們大量的工作都用在定位元素上,定位元素的方式有很多中,定位起來也非常的繁瑣。如果將這些程式碼全部放在程式碼中,不去好好的管理,程式碼會顯示非常的冗餘,而且不容易維護。所以將這些繁瑣的定位,封裝到一些頁面物件中,用例只需要去呼叫就可以了。

2、為什麼要使用POM模式

少數的自動化測試用例維護起來看起來是很容易的。但隨著時間的遷移,測試套件將持續的增長指令碼也將變得越來越臃腫龐大。如果變成我們需要維護10個頁面,100個頁面,甚至1000個呢?而且頁面元素很多是公用的,所以頁面元素的任何改變都會讓我們的指令碼維護變得繁瑣複雜,而且變得耗時易出錯。

也就是說頁面中有一個按鈕"元素A"。該元素A在十個測試用例中都被用到了,如果元素A被前端更新了,我就要去修改這十個自動化用例所用道元素A的地方。如果有100個、1000個用例用到了元素A,那我可就瘋了。

而POM設計模式,會把公共的元素抽取出來,該元素被前端修改,只需要更新該元素的定位方式即可,用例不需要改動。換句話說,不管我多少測試用例,用到了該元素,我只重新修改元素的定位方式,重新能夠獲得該元素即可。

3、POM的優勢

在自動化測試中,引入了Page Object Model(POM):頁面物件模式,能讓我們的測試程式碼變得可讀性更好,高可維護性,高複用性。

還有如下優勢:

  1. 讓Ul自動化更早介入專案中,可專案開發完再進行元素定位的適配與除錯。
    換句話說元素定位器分離出來寫,最後根據前端開發出來的頁面,再根據頁面編寫編寫元素定位器,前期可以做一些其他的工作。
  2. POM設計模式將頁面元素定位和業務操作流程分開,分離了測試物件和測試指令碼(物件庫與用例分離),使得我們更好的複用測試物件。
  3. 如果Ul頁面元素更改,測試指令碼不需要更改,只需要更改頁面物件中的某些程式碼就可以。
  4. POM設計模式能讓我們的測試程式碼變得更加優化,提高了可讀性,可維護性,可複用性。
  5. 可多人共同維護開發指令碼,利於團隊協作。

4、POM模式封裝思路

(1)POM模式將頁面分成三層

  1. 表現層:頁面中可見的元素,都屬於表現層。(元素定位器的編寫)
  2. 操作層:對頁面可見元素的操作。點選、輸入、拖拽等。
  3. 業務層:在頁面中對若干元素操作後所實現的功能。(就是測試用例)

(2)POM模式的核心要素(重點)

  1. 在POM模式中將公共方法統一封裝成到一個BasePage 類中,換句話說該基類對Selenium的常用操作做二次封裝。
  2. 每個頁面對應一個Page類,Page類都需要繼承BasePage,通過 driver 來管理本Page類中的元素,並將Page類中的操作封裝成一個個的方法。
    換句話說,就是Page類中封裝頁面表現層和操作層。
  3. TestCase繼承 unittest.Testcase 類,並且依賴 Page 類,從而實現相應的測試步驟。

(3)總結

就是按照系統或模組 —> 其中包含哪些被測頁面 —> 頁面中的哪些元素

換句話說,元素被頁面管理,頁面被模組管理。

  • 根據頁面來進行管理例:
    例如:測式xx頁面,需要用到的元素,把所有的元素定位器編寫出來。
  • 頁面根據系統或者模組來管理例如:
    例如:xx系統或模組,涉及到哪幾個頁面元素。

(4)非POM和POM對比圖

image

(5)POM設計模式核心架構圖

image

5、對POM小結:

  • POM是selenium webdriver自動化測試實踐物件庫設計模式。
  • POM使得測試指令碼更易於維護。
  • POM通過物件庫方式進一步優化了元素、用例、資料的維護組織。

(二)將普通的Selenium程式碼封裝成POM模式

1、案例說明:

提示:這裡只是提供一種封裝的思路,小夥伴們可以根據自己的實際情況,按需封裝。

以下是簡單普通的登入測試用例

# 1. 匯入包
from selenium import webdriver
import time

# 2. 開啟谷歌瀏覽器(獲取瀏覽器操作物件)
driver = webdriver.Chrome()

# 3. 開啟快遞100網站
url = "https://sso.kuaidi100.com/sso/authorize.do"
driver.get(url)
time.sleep(3)

# 4. 登陸網站
driver.find_element_by_id("name").send_keys('xxxxxxxxxxx')
driver.find_element_by_id("password").send_keys('xxxxxx')
driver.find_element_by_id("submit").click()
time.sleep(3)

# 5. 關閉瀏覽器
driver.quit()

那我們如何進行一個改造升級呢?

2、加入unittest測試框架

# 1. 匯入包
from selenium import webdriver
import time
import unittest


# 定義測試類
class TestCaseLogin(unittest.TestCase):
    def setUp(self) -> None:
        """
            前置函式
            用於開啟瀏覽器,連線資料庫,初始化資料等操作
        """
        # 2. 開啟谷歌瀏覽器(獲取瀏覽器操作物件)
        self.driver = webdriver.Chrome()

        # 3. 開啟快遞100網站
        url = "https://sso.kuaidi100.com/sso/authorize.do"
        self.driver.get(url)
        time.sleep(3)

    def tearDown(self) -> None:
        """
            後置函式
            用於關閉瀏覽器,斷開資料庫連線,清理測試資料等操作
        """
        # 5. 關閉瀏覽器
        self.driver.quit()

    def testLogin(self):
        """登陸測試用例"""
        self.driver.find_element_by_id("name").send_keys('xxxxxxxxxxx')
        self.driver.find_element_by_id("password").send_keys('xxxxxx')
        self.driver.find_element_by_id("submit").click()
        time.sleep(3)


if __name__ == '__main__':
    unittest.main()

如果有不清楚unittest測試框架的小夥伴可以檢視我以前的unittest測試框架部落格有4篇,簡單易懂。

3、加入元素顯示等待

我們上邊的示例中,用的是固定的等待時間,我們需要有話一下程式碼的效率,加入元素的顯示等待。

關於元素顯示等待請看:元素等待的使用

Seleniun的EC模組:EC模組的使用

# 1. 匯入包
from selenium import webdriver
import time
import unittest
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


# 定義測試類
class TestCaseLogin(unittest.TestCase):
    def setUp(self) -> None:
        """
            前置函式
            用於開啟瀏覽器,連線資料庫,初始化資料等操作
        """
        # 2. 開啟谷歌瀏覽器(獲取瀏覽器操作物件)
        self.driver = webdriver.Chrome()

        # 3. 開啟快遞100網站
        url = "https://sso.kuaidi100.com/sso/authorize.do"
        self.driver.get(url)
        time.sleep(2)

    def tearDown(self) -> None:
        """
            後置函式
            用於關閉瀏覽器,斷開資料庫連線,清理測試資料等操作
        """
        # 5. 關閉瀏覽器
        time.sleep(2)
        self.driver.quit()

    def testLogin(self):
        """登陸測試用例"""
        # 編寫定位器
        name_input_locator = ("id", "name")
        passwd_input_locator = ("id", "password")
        submit_button_locator = ("id", "submit")

        # 等待元素出現在操作元素
        WebDriverWait(self.driver, 5).until(EC.visibility_of_element_located(name_input_locator))
        WebDriverWait(self.driver, 5).until(EC.visibility_of_element_located(passwd_input_locator))
        WebDriverWait(self.driver, 5).until(EC.visibility_of_element_located(submit_button_locator))
        self.driver.find_element_by_id("name").send_keys('xxxxxxxxxxx')
        self.driver.find_element_by_id("password").send_keys('xxxxxx')
        self.driver.find_element_by_id("submit").click()


if __name__ == '__main__':
    unittest.main()

4、引入POM模式

我們發現上面的程式碼越來越亂,程式碼冗餘,不利於維護,可讀性差,不可複用。

(1)改造案例思路:

  • 第一, 我們要分離測試物件(元素物件)和測試指令碼(用例指令碼),那麼我們分別建立兩個指令碼檔案,分別為:
    • LoginPage.py 用於定義頁面元素物件,每一個元素都封裝成元件(可以看做存放頁面元素物件的倉庫)
    • TestCaseLogin.py 測試用例指令碼。
  • 第二,抽取出公共方法定義在base.py檔案中,每個Page類都要繼承這個base.py檔案,也就是每Page類都能使用base類中的方法,來操作頁面中的元素,同時也可以在每個Page類中定義自己獨有的方法,解決工作中的實際需求。
  • 第三,設計實現思想,一切元素和元素的操作元件化定義在Page頁面,用例指令碼頁面,通過呼叫Page中的元件物件,進行拼湊成一個登入指令碼。

(2)封裝公共操作在base類

把一些公共的方法放到此類中,這個類將被PO物件繼承。

"""
    封裝公共方法
"""
from selenium import webdriver
import time
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


class Base:
    def __init__(self, browser="chrome"):
        """
        初始化driver
        :param browser:瀏覽器名稱
        """
        if browser == "chrome":
            self.driver = webdriver.Chrome()
        elif browser == "firefox":
            self.driver = webdriver.Firefox()
        elif browser == "ie":
            self.driver = webdriver.Ie()
        else:
            self.driver = None
            print("請輸入正確的瀏覽器,例如:chrome,firefox,ie")

    def open_url(self, url):
        """
        開啟地址
        :param url: 被測地址
        :return:
        """
        self.driver.get(url)
        time.sleep(2)

    def find_element(self, locator, timeout=10):
        """
        定位單個元素,如果定位成功返回元素本身,如果失敗,返回False
        :param locator: 定位器,例如("id","id屬性值")
        :return: 元素本身
        """
        try:
            element = WebDriverWait(self.driver, timeout).until(EC.presence_of_element_located(locator))
            return element
        except:
            print(f"{locator}元素沒找到")
            return False

    def click(self, locator):
        """
        點選元素
        :return:
        """
        element = self.find_element(locator)
        element.click()

    def send_keys(self, locator, text):
        """
        元素輸入
        :param locator: 定位器
        :param text: 輸入內容
        :return:
        """
        element = self.find_element(locator)
        element.clear()
        element.send_keys(text)

    def close(self):
        """
        關閉瀏覽器
        :return:
        """
        time.sleep(2)
        self.driver.quit()


if __name__ == '__main__':
    base = Base()
    base.open_url("https://sso.kuaidi100.com/sso/authorize.do")
    base.close()

(3)每個頁面對應一個Page類

定位元素的定位器和操作元素方法分離開,元素定位器全部放一起,然後每一個操作元素動作寫成一個方法。

"""
    管理登陸頁面所有的元素,
    以及操作這些元素所用的方法。
"""
from common.base import Base


class LoginPage(Base):
    # 編寫定位器和頁面屬性
    name_input_locator = ("id", "name")
    passwd_input_locator = ("id", "password")
    submit_button_locator = ("id", "submit")
    username = 'xxxxxxxxxxx'
    userpasswd = 'xxxxxx'
    url = 'https://sso.kuaidi100.com/sso/authorize.do'

    # """封裝元素操作"""
    # 輸入使用者名稱
    def name_imput(self):
        self.send_keys(self.name_input_locator, self.username)

    # 輸入密碼
    def passwd_imput(self):
        self.send_keys(self.passwd_input_locator, self.userpasswd)

    # 點選登陸
    def click_submit(self):
        self.click(self.submit_button_locator)


if __name__ == '__main__':
    base = Base('firefox')
    base.open_url(url=LoginPage.url)

(4)原登陸案例封裝完成程式碼

測試方法及測試類的執行都在此檔案中。

# 1. 匯入包
import unittest
from pages.login_page import LoginPage


# 定義測試類
class TestCaseLogin(unittest.TestCase):
    def setUp(self) -> None:
        self.driver = LoginPage()
        self.driver.open_url(LoginPage.url)

    def tearDown(self) -> None:
        # 5. 關閉瀏覽器
        self.driver.close()

    def testLogin(self):
        """登陸測試用例"""
        self.driver.name_imput()
        self.driver.passwd_imput()
        self.driver.click_submit()


if __name__ == '__main__':
    unittest.main()

提示:最後我們在使用測試套件來執行測試用例的時候,就定位這些testcase檔案就好。

5、總結

雖然該實現方法看上去複雜多了,但其中的設計好處是不同層關心不同的問題。

  • 頁面物件只關心元素的定位。
  • 測試用例只關心測試資料。

使用POM進行重新構造程式碼結構後,發現程式碼測試用例程式碼的可讀性提高很多。

定義好的PageObject元件可以重複在其它的指令碼中進行使用,減少了程式碼的工作量,也方便對指令碼進行後期的維護管理,當元素屬性發生變化時,我們只需要對一個PageObaject頁面中的物件元件定義進行更改即可。

相關文章