一狂打,引入問(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)目重斑。