Page Object設(shè)計(jì)模式

一狂打,引入問(wèn)題

在之前的博客中泥技,測(cè)試腳本是使用線性模式來(lái)編寫的贸宏,如下:
注意:本博客所有代碼僅為示例

# -*- coding:utf-8 -*-
# @author: 給你一頁(yè)白紙

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("啟動(dòng)今日頭條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")   # 輸入賬號(hào)
        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() # 點(diǎn)擊登錄
    except Exception as e:
        logging.error("登錄錯(cuò)誤,原因?yàn)椋簕}".format(e))
    # 斷言是否登錄成功
    toast_el = is_toast_exist(driver, "登錄成功")
    assert toast_el, True
    logging.info("登陸成功...")

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

但是威创,這種線性模式存在以下等缺點(diǎn):

  • 元素定位屬性和代碼混雜在一起落午,不方便后續(xù)維護(hù)
  • 公共模塊和業(yè)務(wù)模塊混合在一起,顯得代碼冗余
  • 適用測(cè)試場(chǎng)景太單一

在業(yè)務(wù)場(chǎng)景較為簡(jiǎn)單時(shí)這樣寫似乎沒(méi)問(wèn)題那婉,但一旦遇到產(chǎn)品需求變更、業(yè)務(wù)邏輯比較復(fù)雜党瓮,需要維護(hù)的時(shí)就會(huì)非常麻煩详炬。

二,優(yōu)化思路

  • 將公共方法(如:is_toast_exist()寞奸,日志記錄器等)抽離出來(lái)呛谜,放入單獨(dú)模塊
  • 將元素定位方法、元素屬性值枪萄、測(cè)試業(yè)務(wù)代碼分離
  • 登錄操作單獨(dú)封裝成一個(gè)模塊
  • 使用Unittest單元測(cè)試框架管理并執(zhí)行測(cè)試用例

基于以上思路隐岛,我們就需要引入Page Object測(cè)試設(shè)計(jì)模式。

三瓷翻,Page Object 設(shè)計(jì)模式

Page Object模式是Selenium中的一種測(cè)試設(shè)計(jì)模式聚凹,是Selenium割坠、appium自動(dòng)化測(cè)試項(xiàng)目的最佳設(shè)計(jì)模式之一。Page Object的通常的做法是妒牙,將公共方法彼哼、邏輯操作(元素定位、操作步驟)湘今、測(cè)試用例敢朱、測(cè)試數(shù)據(jù)和測(cè)試驅(qū)動(dòng)相互分離,可以理解為將測(cè)試項(xiàng)目進(jìn)行如下分層:

  • 公共方法層
  • 邏輯操作層(元素定位摩瞎,測(cè)試步驟)
  • 測(cè)試用例層(測(cè)試業(yè)務(wù))
  • 測(cè)試數(shù)據(jù)層
  • 測(cè)試驅(qū)動(dòng)層(執(zhí)行測(cè)試用例)

公共方法層拴签,包括公共方法或基礎(chǔ)方法。
邏輯操作層旗们,主要是將每一個(gè)頁(yè)面或該頁(yè)面需要測(cè)試的某個(gè)功能涉及到的元素設(shè)計(jì)為一個(gè)class蚓哩。
測(cè)試用例層,只需調(diào)用邏輯操作層中對(duì)應(yīng)頁(yè)面的class即可蚪拦。
測(cè)試數(shù)據(jù)層杖剪,即測(cè)試數(shù)據(jù)分離,包括配置數(shù)據(jù)和測(cè)試數(shù)據(jù)驰贷,如Capabilities盛嘿、登錄賬號(hào)密碼。
測(cè)試驅(qū)動(dòng)層括袒,執(zhí)行整個(gè)測(cè)試并生成測(cè)試報(bào)告次兆。

四,Page Object + Unittest 測(cè)試項(xiàng)目示例

使用Page Object模式锹锰,Unittest管理測(cè)試用例芥炭。unittest框架請(qǐng)參考博客Unittest單元測(cè)試框架

1,公共方法層

封裝App啟動(dòng)的Capabilities配置信息恃慧,baseDriver.py

# -*- coding:utf-8 -*-
# @author: 給你一頁(yè)白紙

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"]

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

if __name__ == '__main__':
    android_driver()

封裝基礎(chǔ)類,basePage.py

# -*- coding:utf-8 -*-
# @author: 給你一頁(yè)白紙

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請(qǐng)參考博客Python日志采集

2怠蹂,邏輯操作層

封裝登錄善延,login_page.py

# -*- coding:utf-8 -*-
# @author: 給你一頁(yè)白紙

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")    # 登錄頁(yè)用戶名輸入框
    password_inputBox = (By.ID, "com.ss.android.article.news:id/c5")    # 登錄頁(yè)密碼輸入框
    loginBtn = (By.ID, "com.ss.android.article.news:id/a2o")    # 登錄頁(yè)登錄按鈕

    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,測(cè)試用例層

封裝setUp城侧、tearDown易遣,baseTest.py

# -*- coding:utf-8 -*-
# @author: 給你一頁(yè)白紙

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()

封裝測(cè)試用例,test_login.py

# -*- coding:utf-8 -*-
# @author: 給你一頁(yè)白紙

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("正確的賬號(hào)嫌佑、密碼登錄")
        l = LoginPage(self.driver)
        l.login_action("13838380000", "123456")
        result = l.is_toast_exist("登錄成功")
        self.assertTrue(result)

    def test_login_error(self):
        logger.info("正確的賬號(hào)豆茫、錯(cuò)誤的密碼登錄")
        l = LoginPage(self.driver)
        l.login_action("13838380000", "111111")
        result = l.is_toast_exist("密碼錯(cuò)誤")
        self.assertTrue(result)

4侨歉,測(cè)試數(shù)據(jù)層

Capabilities配置數(shù)據(jù),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

測(cè)試用例test_login.py中澜薄,正確的賬號(hào)为肮、正確密碼、錯(cuò)誤密碼也可以配置在Yaml文件中肤京,即數(shù)據(jù)分離颊艳,使用時(shí)讀取即可。Yaml文件的使用可參考博客Python讀寫Yaml文件忘分。

5棋枕,測(cè)試驅(qū)動(dòng)層

執(zhí)行測(cè)試模塊,run.py

# -*- coding:utf-8 -*-
# @author: 給你一頁(yè)白紙

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自動(dòng)化測(cè)試報(bào)告",
                                       description="測(cè)試用例情況")

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

6妒峦,示例目錄結(jié)構(gòu)

運(yùn)行run.py模塊就能執(zhí)行整個(gè)測(cè)試項(xiàng)目重斑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市肯骇,隨后出現(xiàn)的幾起案子窥浪,更是在濱河造成了極大的恐慌,老刑警劉巖笛丙,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漾脂,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡胚鸯,警方通過(guò)查閱死者的電腦和手機(jī)骨稿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)姜钳,“玉大人坦冠,你說(shuō)我怎么就攤上這事「缜牛” “怎么了辙浑?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拟糕。 經(jīng)常有香客問(wèn)我判呕,道長(zhǎng),這世上最難降的妖魔是什么已卸? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任佛玄,我火速辦了婚禮硼一,結(jié)果婚禮上累澡,老公的妹妹穿的比我還像新娘。我一直安慰自己般贼,他們只是感情好愧哟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布奥吩。 她就那樣靜靜地躺著,像睡著了一般蕊梧。 火紅的嫁衣襯著肌膚如雪霞赫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天肥矢,我揣著相機(jī)與錄音端衰,去河邊找鬼。 笑死甘改,一個(gè)胖子當(dāng)著我的面吹牛旅东,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播十艾,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼抵代,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了忘嫉?” 一聲冷哼從身側(cè)響起荤牍,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎庆冕,沒(méi)想到半個(gè)月后康吵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡愧杯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年涎才,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片力九。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡耍铜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出跌前,到底是詐尸還是另有隱情棕兼,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布抵乓,位于F島的核電站伴挚,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏灾炭。R本人自食惡果不足惜茎芋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蜈出。 院中可真熱鬧田弥,春花似錦、人聲如沸铡原。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至只泼,卻和暖如春剖笙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背请唱。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工弥咪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人十绑。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓酪夷,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親孽惰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子晚岭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355