Page Object設計模式

給你一頁白紙發表於2020-10-13

一,引入問題

在之前的部落格中,測試指令碼是使用線性模式來編寫的,如下:
注意:本部落格所有程式碼僅為示例

# -*- coding:utf-8 -*-
# @author: 給你一頁白紙

import logging
from appium import webdriver
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait
from appium.webdriver.common.mobileby import MobileBy as By

logging.basicConfig(filename='./testLog.log', level=logging.INFO,
                    format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s')

def android_driver():
    desired_caps = {
        "platformName": "Android",
        "platformVersion": "10",
        "deviceName": "PCT_AL10",
        "appPackage": "com.ss.android.article.news",
        "appActivity": ".activity.MainActivity",
        "unicodeKeyboard": True,
        "resetKeyboard": True,
        "noReset": True,
    }
    logging.info("啟動今日頭條APP...")
    driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps)
    return driver

def is_toast_exist(driver, text, timeout=20, poll_frequency=0.1):
    '''
    判斷toast是否存在,是則返回True,否則返回False
    '''
    try:
        toast_loc = (By.XPATH, ".//*[contains(@text, %s)]" % text)
        WebDriverWait(driver, timeout, poll_frequency).until(
            ec.presence_of_element_located(toast_loc)
        )
        return True
    except:
        return False

def login_test(driver):
    '''登入今日頭條操作'''
    logging.info("開始登陸今日頭條APP...")
    try:
            driver.find_element_by_id("com.ss.android.article.news:id/bu").send_keys("xxxxxxxx")   # 輸入賬號
        driver.find_element_by_id("com.ss.android.article.news:id/c5").send_keys("xxxxxxxx")   # 輸入密碼
        driver.find_element_by_id("com.ss.android.article.news:id/a2o").click() # 點選登入
    except Exception as e:
        logging.error("登入錯誤,原因為:{}".format(e))
    # 斷言是否登入成功
    toast_el = is_toast_exist(driver, "登入成功")
    assert toast_el, True
    logging.info("登陸成功...")

if __name__ == '__main__':
    driver = android_driver()
    login_opera(driver)

但是,這種線性模式存在以下等缺點:

  • 元素定位屬性和程式碼混雜在一起,不方便後續維護

  • 公共模組和業務模組混合在一起,顯得程式碼冗餘

  • 適用測試場景太單一

在業務場景較為簡單時這樣寫似乎沒問題,但一旦遇到產品需求變更、業務邏輯比較複雜,需要維護的時就會非常麻煩。

二,優化思路

  • 將公共方法(如:is_toast_exist(),日誌記錄器等)抽離出來,放入單獨模組

  • 將元素定位方法、元素屬性值、測試業務程式碼分離

  • 登入操作單獨封裝成一個模組

  • 使用Unittest單元測試框架管理並執行測試用例

基於以上思路,我們就需要引入Page Object測試設計模式。

三,Page Object 設計模式

Page Object模式是Selenium中的一種測試設計模式,是Selenium、appium自動化測試專案的最佳設計模式之一。Page Object的通常的做法是,將公共方法、邏輯操作(元素定位、操作步驟)、測試用例、測試資料和測試驅動相互分離,可以理解為將測試專案進行如下分層:

  • 公共方法層

  • 邏輯操作層(元素定位,測試步驟)

  • 測試用例層(測試業務)

  • 測試資料層

  • 測試驅動層(執行測試用例)

公共方法層,包括公共方法或基礎方法。

邏輯操作層,主要是將每一個頁面或該頁面需要測試的某個功能涉及到的元素設計為一個class。

測試用例層,只需呼叫邏輯操作層中對應頁面的class即可。

測試資料層,即測試資料分離,包括配置資料和測試資料,如Capabilities、登入賬號密碼。

測試驅動層,執行整個測試並生成測試報告。

四,Page Object + Unittest 測試專案示例

使用Page Object模式,Unittest管理測試用例。unittest框架請參考部落格Unittest單元測試框架

1,公共方法層

封裝App啟動的Capabilities配置資訊,baseDriver.py

# -*- coding:utf-8 -*-
# @author: 給你一頁白紙

import yaml
from appium import webdriver
from common.baseLog import logger

def android_driver():
    stream = open("../config/desired_caps", "r")
    data = yaml.load(stream, Loader=yaml.FullLoader)

    desired_caps = {}
    desired_caps["platformName"] = data["Android"],
    desired_caps["platformVersion"] = data["platformVersion"],
    desired_caps["deviceName"] = data["deviceName"],
    desired_caps["appPackage"] = data["appPackage"],
    desired_caps["appActivity"] = data["appActivity"],
    desired_caps["unicodeKeyboard"] = data["unicodeKeyboard"],
    desired_caps["resetKeyboard"] = data["resetKeyboard"],
    desired_caps["noReset"] = data["noReset"],
    desired_caps["automationName"] = data["automationName"]

    # 啟動app
    try:
        driver = webdriver.Remote('http://' + str(data['ip']) + ':' + str(data['port']) + '/wd/hub', desired_caps)
        logger.info("APP啟動成功...")
        driver.implicitly_wait(8)
        return driver
    except Exception as e:
        logger.error("APP啟動失敗,原因是:{}".format(e))

if __name__ == '__main__':
    android_driver()

封裝基礎類,basePage.py

# -*- coding:utf-8 -*-
# @author: 給你一頁白紙

from common.baseLog import logger
from selenium.webdriver.support.ui import WebDriverWait
from appium.webdriver.common.mobileby import MobileBy as By
from selenium.webdriver.support import expected_conditions as EC

class BasePage:
    def __init__(self, driver):
        self.driver = driver

    def get_visible_element(self, locator, timeout=20):
        '''獲取可視元素'''
        try:
            return WebDriverWait(self.driver, timeout).until(
                EC.visibility_of_element_located(locator)
            )
        except Exception as e:
            logger.error("獲取元素失敗:{}".format(e))

    def is_toast_exist(driver, text, timeout=20, poll_frequency=0.1):
        '''
        判斷toast是否存在,是則返回True,否則返回False
        '''
        try:
            toast_loc = (By.XPATH, ".//*[contains(@text, %s)]" % text)
            WebDriverWait(driver, timeout, poll_frequency).until(
                EC.presence_of_element_located(toast_loc)
            )
            return True
        except:
            return False

日誌模組baseLog.py請參考部落格Python日誌採集

2,邏輯操作層

封裝登入,login_page.py

# -*- coding:utf-8 -*-
# @author: 給你一頁白紙

from common.baseLog import logger
from common.basePage import BasePage
from appium.webdriver.common.mobileby import MobileBy as By

class LoginPage(BasePage):

    username_inputBox = (By.ID, "com.ss.android.article.news:id/bu")    # 登入頁使用者名稱輸入框
    password_inputBox = (By.ID, "com.ss.android.article.news:id/c5")    # 登入頁密碼輸入框
    loginBtn = (By.ID, "com.ss.android.article.news:id/a2o")    # 登入頁登入按鈕

    def login_action(self, username, password):
        logger.info("開始登入...")
        logger.info("輸入使用者名稱:{}".format(username))
        self.get_visible_element(self.username_inputBox).send_keys(username)
        logger.info("輸入密碼:{}".format(password))
        self.get_visible_element(self.password_inputBox).send_keys(password)
        self.get_visible_element(self.loginBtn).click()

3,測試用例層

封裝setUp、tearDown,baseTest.py

# -*- coding:utf-8 -*-
# @author: 給你一頁白紙

import time
import unittest
from common.baseDriver import android_driver

class StartEnd(unittest.TestCase):
    def setUp(self) -> None:
        self.driver = android_driver()

    def tearDown(self) -> None:
        time.sleep(2)
        self.driver.close_app()

封裝測試用例,test_login.py

# -*- coding:utf-8 -*-
# @author: 給你一頁白紙

from common.baseLog import logger
from common.baseTest import StartEnd
from page.login_page import LoginPage

class LoginTest(StartEnd):

    def test_login_right(self):
        logger.info("正確的賬號、密碼登入")
        l = LoginPage(self.driver)
        l.login_action("13838380000", "123456")
        result = l.is_toast_exist("登入成功")
        self.assertTrue(result)

    def test_login_error(self):
        logger.info("正確的賬號、錯誤的密碼登入")
        l = LoginPage(self.driver)
        l.login_action("13838380000", "111111")
        result = l.is_toast_exist("密碼錯誤")
        self.assertTrue(result)

4,測試資料層

Capabilities配置資料,desired_caps.yml

appActivity: .activity.MainActivity
appPackage: com.ss.android.article.news
deviceName: newDeviceName
platformName: Android
platformVersion: newPlatformVersion
automationName: UiAutomator2
unicodeKeyboard: true
resetKeyboard: true
noReset: true
ip: 127.0.0.1
port: 4723

測試用例test_login.py中,正確的賬號、正確密碼、錯誤密碼也可以配置在Yaml檔案中,即資料分離,使用時讀取即可。Yaml檔案的使用可參考部落格Python讀寫Yaml檔案

5,測試驅動層

執行測試模組,run.py

# -*- coding:utf-8 -*-
# @author: 給你一頁白紙

import time
import unittest
import HTMLTestRunner

now = time.strftime("%Y-%m-%d_%H_%M_%S")
report_dir = './report/'
fp = open(report_dir + now + "_report.html", 'wb')
runner = HTMLTestRunner.HTMLTestRunner(stream=fp,
                                       title="App自動化測試報告",
                                       description="測試用例情況")

test_dir='./testcase'
suite = unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py')
runner.run(suite)
fp.close()

6,示例目錄結構

執行run.py模組就能執行整個測試專案。

相關文章