Page Object設計模式

一,引入問題

在以前的博客中,測試腳本是使用線性模式來編寫的,以下:
注意:本博客全部代碼僅爲示例html

# -*- 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)

可是,這種線性模式存在如下等缺點:python

  • 元素定位屬性和代碼混雜在一塊兒,不方便後續維護android

  • 公共模塊和業務模塊混合在一塊兒,顯得代碼冗餘web

  • 適用測試場景太單一設計模式

在業務場景較爲簡單時這樣寫彷佛沒問題,但一旦遇到產品需求變動、業務邏輯比較複雜,須要維護的時就會很是麻煩。app

二,優化思路

  • 將公共方法(如: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模塊就能執行整個測試項目。

相關文章
相關標籤/搜索