UI自動化框架 基於selenium+pytest和PO分層思想
最近在編寫UI自動化框架,現在將一些碎片化東西進行梳理,便於記憶
同時,為了方便於各個模組的獨立管理,以及秉承高複用,低耦合的思想,這裡是根據PO模型編寫,同時將所有的模組進行了獨立,頁面和元素,以及用例和操作
框架用到的所有分層,梳理一下每個包的用途
-
.pytest_cache 這個是使用pytest框架系統預設匯入的
-
commom公共管理方法
common_handle: 這裡可以理解為base_page,這裡對selenium裡的方法進行了二次封裝,同時對於一些操作,會在程式碼段裡做註釋,就不過多敘述
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/14 15:15
"""
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from JPT_UITEST.Config.path_name import PathName
from JPT_UITEST.common import time
from JPT_UITEST.common.log_handle import logger
class BasePage:
# 初始化,傳入一個driver
def __init__(self, driver: WebDriver):
self.driver = driver
# 二次封裝元素等待,如果錯誤,記錄日誌,並截圖儲存
def wait(self, loc, filename) :
"""
元素等待
:param loc: 等待的元素
:param filename: 截圖名字
:return:
這裡使用的是隱式等待,同時將隱式等待和元素是否可見的判斷進行了結合,這樣更加穩定!
"""
logger.info('{}正待等待元素{}'.format(filename, loc))
try:
WebDriverWait(self.driver, timeout=30).until(EC.visibility_of_element_located(loc))
#首先是隱式等待表示式(driver物件,等待時長)
#同時每0.5秒會檢視一次,檢視元素是否出現,如果超過30s未出現,則報錯timeout
#until()是等待元素可見,這裡加入了元素是否可見的判斷
except Exception as e:
self.error_screenshots(filename)
logger.exception('元素等待錯誤發生:{}元素為{}'.format(e, loc))
raise
def error_screenshots(self, name):
"""
儲存截圖
:param name:根據被呼叫傳入的名字,生成png的圖片
:return:
"""
try:
file_path = PathName.screenshots_path
times = time.time_sj()
filename = file_path + times + '{}.png'.format(name)
self.driver.get_screenshot_as_file(filename)
logger.info("正在儲存圖片:{}".format(filename))
except Exception as e:
logger.error('圖片報存錯誤:{}'.format(e))
raise
def get_ele(self, loc, filename):
"""
查詢元素
:param loc:
:param filename:
:return:
"""
logger.info('{}正在查詢元素:{}'.format(filename, loc))
try:
# 這裡使用的是find_element查詢單個元素,這裡需要傳入的是一個表示式,需要告訴driver物件使用的是什麼定位方法,以及元素定位!
# By是繼承了selenium裡面的8大定位方法,所以框架裡操作元素的皆是By.XPATH或者By.id等等
# 同時因為需要傳入的是一個表示式,而By.XPATH是一個元組,這裡做了解包處理
ele = self.driver.find_element(*loc)
except Exception as e:
logger.exception('查詢元素失敗:')
self.error_screenshots(filename)
raise
else:
return ele
def send_key(self, loc, name, filename):
"""
輸入文字
:param loc:元素
:param filename:截圖名字
:param name: 輸入的名字
:return:
"""
logger.info('{}正在操作元素{},輸入文字{}'.format(filename, loc, name))
self.wait(loc, filename)
try:
self.get_ele(loc, filename).send_keys(name)
except:
logger.exception('元素錯誤 {}:')
self.error_screenshots(filename)
raise
def click_key(self, loc, filename):
"""
元素點選
:param loc:
:param filename:
:return:
"""
logger.info('{}正在操作元素{}'.format(filename, loc))
self.wait(loc, filename)
try:
self.get_ele(loc, filename).click()
except Exception as e:
logger.exception('點選元素錯誤:{}'.format(e))
self.error_screenshots(filename)
raise
def get_ele_text(self, loc, filename):
"""
獲取元素文字
:param loc:
:param filename:
:return:
"""""
logger.info('{}正在獲取文字{}'.format(filename, loc))
self.wait(loc, filename)
ele = self.get_ele(loc, filename)
try:
text = ele.text
logger.info('獲取文字成功{}'.format(text))
return text
except:
logger.exception('獲取文字錯誤:')
self.error_screenshots(filename)
def get_ele_attribute(self, loc, attribute_name, filename):
"""
獲取元素屬性
:param loc:
:param attribute_name:
:param filename:
:return:
"""
logger.info('{}正在獲取元素{}的屬性'.format(filename, loc))
self.wait(loc, filename)
ele = self.get_ele(loc, filename)
try:
value = ele.get_attribute(attribute_name)
logger.info('獲取屬性成功{}'.format(value))
return value
except:
logger.exception('獲取屬性失敗')
self.error_screenshots(filename)
def wait_ele_click(self, loc, filename):
logger.info('{}正待等待可點選元素{}'.format(filename, loc))
try:
WebDriverWait(self.driver, timeout=20).until(EC.element_to_be_clickable(loc))
# logger.info('等待可點選元素{}'.format(loc))
except:
self.error_screenshots(filename)
logger.exception('等待可點選元素錯誤:元素為{}'.format(loc))
raise
def switch_to_iframe(self, loc, filename):
try:
WebDriverWait(self.driver, 20).until(EC.frame_to_be_available_and_switch_to_it(loc))
logger.info('正在進入巢狀頁面:{}'.format(loc))
except:
logger.exception('進入巢狀頁面失敗{}'.format(loc))
self.error_screenshots(filename)
def click_wait_ele(self, loc, filename):
logger.info('正在等待{}中的可點選元素出現{}'.format(filename, loc))
self.wait_ele_click(loc, filename)
try:
self.get_ele(loc, filename).click()
logger.info('正在{}中點選元素{}'.format(filename, loc))
except:
logger.info('在{}當中點選{}元素失敗'.format(filename, loc))
self.error_screenshots(filename)
這裡應該有部分同學會對初始化時的位置引數比較疑惑,為什麼是 driver: WebDriver這種寫法!
這裡的寫法,是因為我們在封裝時,不能在這裡去例項driver會話物件!不然所有頁面使用的就全部是一個會話物件了
但是如果不例項呢,pycharm就不知道driver是什麼,就無法使用後續的一些操作,比如driver.find_element.by.id()這種方法!
所以我們就在初始化時,宣告告訴了編譯器,driver是一個webdriver物件
同時,這裡二次封裝的主要思想為:要做什麼->日誌列印->等待元素->判斷->截圖這樣可以清晰的知道,那個地方出了問題,有日誌,有截圖
2.common_mysql:
這裡是預留封裝mysql的檔案,因為暫時用不到,就沒使用
3.log_handle:
這裡封裝的是log日誌模組
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/11/23 17:19
"""
import logging
import os
from JPT_UITEST.Config.common_data import Context
from JPT_UITEST.Config.path_name import PathName
log_path_name = os.path.join(PathName.logs_path, Context.log_name)
# print(log_path_name)
class LogHandel(logging.Logger):
"""定義日誌類"""
def __init__(self, name, file, level='DEBUG',
fmt="%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s"):
super().__init__(name)
self.setLevel(level)
file_headers = logging.FileHandler(file)
file_headers.setLevel(level)
self.addHandler(file_headers)
fmt = logging.Formatter(fmt)
file_headers.setFormatter(fmt)
logger = LogHandel(Context.log_name, log_path_name, level=Context.level)
if __name__ == '__main__':
log = logger
log.warning('測試1')
這裡是通用模板,不多說
4. time.py
這裡是生成時間
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/15 14:56
"""
import datetime
def time_sj():
sj = datetime.datetime.now().strftime('%m-%d-%H-%M-%S')
return sj
Config 這裡我存放的是路徑,以及公共操作裡會用到的資料
common_data.py
這裡就放了一些關於日誌的等級和level
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/15 13:47
"""
class Context:
log_name = 'Ui自動化.txt'
level = 'DEBUG'
path_name.py
這裡是存放了所有會用到的存放路徑,以及讀取資料路徑
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/15 13:47
"""
import os
class PathName:
# 初始路徑F:\jintie\JPT_UITEST
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# F:\jintie\JPT_UITEST\Options\
options_path = os.path.join(dir_path, 'Options')
# Logs檔案目錄地址
logs_path = os.path.join(options_path, 'Logs/')
# report測試報告路徑
report_path = os.path.join(options_path, 'report/')
screenshots_path = os.path.join(options_path, 'error_screenshots/')
case_name = os.path.join(dir_path, 'testcases/')
data檔案包中存放的是專案相關的全域性資料,和測試資料
Global_Data:存放了所有全域性資料
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/14 15:21
"""
class GlobalData:
# 域名
host = ''
# url
BackGround = host + '/login'
Test_data:
test_data.py: 存放的測試資料
資料分離的好處在於,如果資料發生改變,我只需要在這個檔案中將資料做修改即可,不需要去做其他的程式碼改動
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/14 15:56
"""
class TestData:
# 正確賬號密碼
just_data = {
'username': '',
'pwd': ''
}
# 錯誤測試資料
test_data = [{'username': '', 'pwd': ''},
{'username': '', 'pwd': ''},
{'username': '', 'pwd': ''}]
lib的包中原本存放的是一些修改過原始碼的三方庫,比如HTMLTestRunner.py和ddt.py
但是後續將unittest改為了pytest所以這裡的資料驅動模組,和測試報告模組變棄用了,不多做敘述
Options:這裡存放的為輸出的日誌,報告,以及錯誤截圖
因為使用了PO模型, PO模型是指在UI自動化中,將頁面、操作、元素、和測試用例全部獨立,減少耦合性,所以這裡我存放的為xpath元素方法
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/14 15:13
"""
“”“涉及了實際專案,所以部分資料做脫敏處理”“”
from selenium.webdriver.common.by import By
class BackGroundLoc:
# 登陸視窗
input_user = (By.XPATH, '//input[@id="name"]')
# 輸入密碼
input_pwd = (By.XPATH, '//input[@id="password"]')
# 選擇平臺下拉框
select_platform_input = (By.XPATH, '//div[@unselectable="on"]')
# 選擇子平臺
select_platform = (By.XPATH, '//span[text()=""]')
# 記住使用者名稱
Re_user_name = (By.XPATH, '//span[@class="ant-checkbox ant-checkbox-checked"]')
# 使用者登入
user_login = (By.XPATH, '//button[@class="ant-btn login-form-button ant-btn-primary ant-btn-lg"]')
title_name = 'XX管理系統'
# error_msg
error_msg = (By.XPATH, '//div[@class="ant-form-explain"]')
這裡是首頁頁面所有的元素定位!這樣做的好處為,後續如果元素髮生了改變,只需要去修改元素即可,不需要對其他地方做出修改!同時,改了這裡,所有用到此元素的地方,都會發生改變
PageObject: 這裡是所有關於頁面操作的封裝
background_home.py是關於後臺首頁的操作
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/14 14:49
"""
import time
from JPT_UITEST.PageLocation.background_home_loc import BackGroundLoc as BLC
from JPT_UITEST.common.common_handle import BasePage
class PlatformLogin(BasePage):
# 輸入賬號密碼
def platform_login_succeed(self, username, pwd):
self.send_key(BLC.input_user, username, '登陸頁_輸入使用者名稱')
self.send_key(BLC.input_pwd, pwd, '登陸頁_輸入密碼')
time.sleep(2)
# 選擇平臺
def select_platform(self):
self.click_key(BLC.select_platform_input, '登陸頁_選擇下拉框')
self.click_key(BLC.select_platform, '登陸頁_選擇平臺')
# 登陸
def login(self):
self.click_key(BLC.user_login, '登陸頁_點選登陸')
# 記錄使用者名稱
def Res_users_name(self):
self.click_key(BLC.Re_user_name, '登陸頁_記錄使用者名稱')
# 登陸後的首頁
def login_home(self):
self.driver.title(BLC.title_name)
# if __name__ == '__main__':
#
# driver = webdriver.Chrome()
# driver.get()
testcases: 這裡存放的是所有測試用例
conftest.py:這裡是pytest的前後置條件,固定名字!不需要匯入到用例中,pytest會自動遍歷
這裡因為我暫時只用到了一個function用例級別的前後置條件,所以只封裝了一個
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/23 14:26
"""
from JPT_UITEST.data.Global_Data.global_data import GlobalData as GD
from JPT_UITEST.common.log_handle import logger
import pytest
from selenium import webdriver
# @pytest.fixture裝飾器,是表示接下來的函式為測試用例的前後置條件
#fixture一共4個級別,預設scope=function(用例,對標為unittest中的setup以及tearDown)
#同時,fixture中包含了前置條件,以及後置條件
#function是每個用例開始和結束執行
#class是類等級,每個測試類執行一次
#modules 是模組級別,也就是當前.py檔案執行一次
#session 是會話級別, 指測試會話中所有用例只執行一次
#pytest中區分前置後置是用關鍵字yield來區分的, yield之前,是前置!yield之後,是後置 yield同行,是返回資料
@pytest.fixture()
def open_browser():
logger.info('-----------正在執行測試用例開始的準備工作,開啟瀏覽器,請求後臺-----------')
driver = webdriver.Chrome()
driver.get(GD.BackGround)
driver.maximize_window()
yield driver
driver.quit()
test_login.py 測試用例
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/14 14:44
"""
import time
import pytest
from JPT_UITEST.PageObject.background_home import PlatformLogin
from JPT_UITEST.PageObject.jinpantao_home import JinpantaoHome
from JPT_UITEST.common.log_handle import logger
from JPT_UITEST.data.Test_Data.test_data import TestData as TD
data = TD.test_data
#pytest.mark.usefixtures()是使用前置條件,括號中填寫要使用的前置條件函式名稱
#因為封裝前置條件的py檔名稱固定,所以這裡不需要匯入,是由pytest自動遍歷查詢
#放在類方法上,是表示測試類下所有測試方法,都會使用這個前置條件
@pytest.mark.usefixtures('open_browser')
class TestLogin:
# @pytest.mark.parametrize('su_data', TD.just_data)
def test_login_01_succeed(self, open_browser, ):
"""
#正向場景
3.輸入賬號密碼,點選登陸
4.選擇對應子平臺
6.判斷首頁元素是否可見
:return:
"""
logger.info('+++++正在執行正向登陸測試用例+++++')
try:
pf = PlatformLogin(open_browser)
pf.platform_login_succeed(TD.just_data['username'], TD.just_data['pwd'])
pf.select_platform()
pf.login()
JT = JinpantaoHome(open_browser)
JT.context_operation()
JT.tag_mg()
JT.money_mg()
JT.article_list()
JT.article_stat()
JT.article_decry()
logger.info('正在執行測試用例:賬號{},密碼{}'.format(TD.just_data['username'], TD.just_data['pwd']))
except Exception as e:
logger.error('用例執行錯誤:{}'.format(e))
raise AssertionError
“”“@pytest.mark.parametrize()裝飾器是用來執行資料驅動的,需要傳入兩個值
1.資料名稱,不固定,自由填寫
2.資料
但是資料名稱要在使用資料驅動的方法裡當作引數傳入,對標ddt當中的傳值接收
”“”
@pytest.mark.parametrize('test_data', TD.test_data)
def test_login_02_fail(self, open_browser, test_data):
"""
3.輸入賬號密碼
4.選擇對應平臺
5.點選登陸
6.檢視失敗提示
:return:
"""
logger.info('+++++正在執行逆向向登陸測試用例+++++')
try:
pf = PlatformLogin(open_browser)
pf.platform_login_succeed(test_data['username'], test_data['pwd'])
# time.sleep(1)
# pf.Res_users_name()
time.sleep(1)
pf.select_platform()
pf.login()
logger.info('正在執行逆向場景用例使用者名稱{},密碼{}'.format(test_data['username'], test_data['pwd']))
except Exception as e:
logger.error('逆向場景用件執行失敗{}'.format(e))
同時pytest當中,更加靈活的是斷言,如果在unittest中,需要使用self.assert.斷言方式(實際結果,預期結果)
但是pytest當中,只需要使用assert 表示式 例如:assert 1+1=2 這種方式即可,如果結果為true,則代表斷言成功!
最後一個檔案是出於習慣使用了一個載入所有用例的.run_all.py
# encoding: utf-8
"""
@author:輝輝
@email: [email protected]
@Wechat: 不要靜音
@time: 2020/12/17 20:13
"""
import datetime
import os
import pytest
from JPT_UITEST.Config.path_name import PathName
from JPT_UITEST.common.log_handle import logger
def run_all():
times = datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
report_name = os.path.join(PathName.report_path, (times + 'report.html'))
logger.info('生成測試用例,存放為{}'.format(report_name))
pytest.main(['-s', '-v', '--html={}'.format(report_name)])
if __name__ == '__main__':
run_all()
pytest執行用例有幾種方法,我這裡使用的是pytest.main()這個方法,實際上就是和在cmd命令符裡使用
pytest -s -v的效果一樣! '–html=xxx.html’是生成html測試用例!
到這裡,就是一個完整的框架,剩下的就是根據專案的不同,去做一些定製化的操作!