Selenium4+Python3系列(十二) - 測試框架的設計與開發
阿新 • • 發佈:2022-12-10
前言
自己從未沒想過能使用python
來做自動化測試框架的設計、開發。
可能有人會好奇說,六哥,你怎麼也用python
寫測試框架了?
領導說:
python你也沒有實際工作經驗,可能就是自己自學的。
聽完,那一刻,我真的特別證明自己,我也行!
框架搭建
整個框架的實現,大約也就1.5天,關於框架的開發並不是很難,主要難在測試報告增加失敗自動截圖功能
和echart的餅子圖
統計功能,兩者的整合花了近半天的時間吧。
效果:
1、核心思想
延續使用Page Object
和Page Factory
思想,使頁面、資料、元素、指令碼進行分離,此處演示僅僅為了講解框架搭建思路,並非為我在公司寫的那套框架,主要使用selenium4+python3+pytest
目錄結構
2、日誌封裝
主要用於方便定位用例指令碼執行步驟,示例程式碼如下:
# -*- coding: utf-8 -*- """ @Time : 2022/12/7 19:36 @Auth : 軟體測試君 @File :LogUtils.py @IDE :PyCharm @Motto:ABC(Always Be Coding) """ import time import os import logging currrent_path = os.path.dirname(__file__) log_path = os.path.join(currrent_path, '../logs') class LogUtils: def __init__(self, log_path=log_path): """ 通過python自帶的logging模組進行封裝 """ self.logfile_path = log_path # 建立日誌物件logger self.logger = logging.getLogger(__name__) # 設定日誌級別 self.logger.setLevel(level=logging.INFO) # 設定日誌的格式 formatter = logging.Formatter('%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s') """在log檔案中輸出日誌""" # 日誌檔名稱顯示一天的日誌 self.log_name_path = os.path.join(self.logfile_path, "log_%s" % time.strftime('%Y_%m_%d')+".log") # 建立檔案處理程式並實現追加 self.file_log = logging.FileHandler(self.log_name_path, 'a', encoding='utf-8') # 設定日誌檔案裡的格式 self.file_log.setFormatter(formatter) # 設定日誌檔案裡的級別 self.file_log.setLevel(logging.INFO) # 把日誌資訊輸出到檔案中 self.logger.addHandler(self.file_log) # 關閉檔案 self.file_log.close() """在控制檯輸出日誌""" # 日誌在控制檯 self.console = logging.StreamHandler() # 設定日誌級別 self.console.setLevel(logging.INFO) # 設定日誌格式 self.console.setFormatter(formatter) # 把日誌資訊輸出到控制檯 self.logger.addHandler(self.console) # 關閉控制檯日誌 self.console.close() def get_log(self): return self.logger logger = LogUtils().get_log() if __name__ == '__main__': logger.info('123') logger.error('error')
3、基礎頁面
用於存放,控制元件及API
的常用操作,示例程式碼如下:
# -*- coding: utf-8 -*- """ @Time : 2022/12/7 19:58 @Auth : 軟體測試君 @File :BasePage.py @IDE :PyCharm @Motto:ABC(Always Be Coding) """ import time from selenium.common import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait as WD from util.LogUtils import LogUtils from util.ParseConFile import ParseConFile logger = LogUtils().get_log() class BasePage(object): """控制元件及API的常用操作""" cf = ParseConFile() def __init__(self, driver, timeout=30): self.byDic = { 'id': By.ID, 'name': By.NAME, 'class_name': By.CLASS_NAME, 'xpath': By.XPATH, 'link_text': By.LINK_TEXT, 'css': By.CSS_SELECTOR } self.driver = driver self.outTime = timeout def find_element(self, by, locator): """ 通過id, name, xpath, css,class....,查詢元素 """ try: logger.info("通過 " + by + " 定位") element = WD(self.driver, self.outTime).until(lambda x: x.find_element(self.byDic.get(by), locator)) except TimeoutException as e: logger.error('請確認元素定位方式,' + e) else: return element def find_elements(self, by, locator): """ 通過id, name, xpath, css,class....,查詢一組元素 """ try: logger.info("通過 " + by + " 定位") elements = WD(self.driver, self.outTime).until(lambda x: x.find_elements(self.byDic.get(by), locator)) except TimeoutException as e: logger.error('請確認元素定位方式,' + e) else: return elements def get_text(self, by, locator): """ 獲取元素文字/屬性資訊 """ logger.info("獲取元素文字成功!") return self.find_element(by, locator).text def open_url(self, url): """開啟瀏覽器""" logger.info("開啟專案首頁:" + url) self.driver.get(url) def quit_browser(self): self.driver.quit() def send_keys(self, by, locator, keys=''): """輸入操作""" logger.info("輸入:" + keys) self.find_element(by, locator).clear self.sleep(1) self.find_element(by, locator).send_keys(keys) def click(self, by, locator): """點選操作""" logger.info("點選按鈕:" + locator) self.find_element(by, locator).click() @staticmethod def sleep(num=0): """強制等待""" logger.info("程式等待:" + str(num) + " 秒") time.sleep(num)
4、登陸頁面
主要用於存放控制元件及元素操作,示例程式碼如下:
# -*- coding: utf-8 -*-
"""
@Time : 2022/12/7 20:27
@Auth : 軟體測試君
@File :LoginPage.py
@IDE :PyCharm
@Motto:ABC(Always Be Coding)
"""
from Page.BasePage import BasePage
from util.LogUtils import LogUtils
from util.ParseConFile import ParseConFile
logger = LogUtils().get_log()
class LoginPage(BasePage):
"""
存放控制元件及元素操作
"""
# 配置檔案讀取元素
do_conf = ParseConFile()
# 使用者名稱輸入框
username = do_conf.get_locator('LoginPage_Elements', 'username')
# 密碼輸入框
password = do_conf.get_locator('LoginPage_Elements', 'password')
# 登入按鈕
loginBtn = do_conf.get_locator('LoginPage_Elements', 'loginBtn')
# 登入失敗的提示資訊
error_msg = do_conf.get_locator('LoginPage_Elements', 'errorMsg')
def login(self, username, password):
"""登入流程"""
self.open()
self.send_username(username)
self.send_password(password)
self.click_login_btn()
msg = self.get_errorMsg()
return msg
def open(self):
self.open_url('http://localhost:8080/login')
def quit(self):
self.quit_browser()
def send_username(self, username):
self.send_keys(*LoginPage.username, username)
def send_password(self, password):
self.send_keys(*LoginPage.password, password)
def click_login_btn(self):
self.click(*LoginPage.loginBtn)
def get_errorMsg(self):
return self.get_text(*LoginPage.error_msg)
if __name__ == "__main__":
pass
5、業務操作
主要用於記錄用例步驟,示例程式碼如下:
# -*- coding: utf-8 -*-
"""
@Time : 2022/12/7 20:27
@Auth : 軟體測試君
@File :LoginPage.py
@IDE :PyCharm
@Motto:ABC(Always Be Coding)
"""
from Page.BasePage import BasePage
from util.LogUtils import LogUtils
from util.ParseConFile import ParseConFile
logger = LogUtils().get_log()
class LoginPage(BasePage):
"""
存放控制元件及元素操作
"""
# 配置檔案讀取元素
do_conf = ParseConFile()
# 使用者名稱輸入框
username = do_conf.get_locator('LoginPage_Elements', 'username')
# 密碼輸入框
password = do_conf.get_locator('LoginPage_Elements', 'password')
# 登入按鈕
loginBtn = do_conf.get_locator('LoginPage_Elements', 'loginBtn')
# 登入失敗的提示資訊
error_msg = do_conf.get_locator('LoginPage_Elements', 'errorMsg')
def login(self, username, password):
"""登入流程"""
self.open()
self.send_username(username)
self.send_password(password)
self.click_login_btn()
msg = self.get_errorMsg()
return msg
def open(self):
self.open_url('http://localhost:8080/login')
def quit(self):
self.quit_browser()
def send_username(self, username):
self.send_keys(*LoginPage.username, username)
def send_password(self, password):
self.send_keys(*LoginPage.password, password)
def click_login_btn(self):
self.click(*LoginPage.loginBtn)
def get_errorMsg(self):
return self.get_text(*LoginPage.error_msg)
if __name__ == "__main__":
pass
6、測試報告之失敗帶截圖
這塊確實很坑,看了很多網上的教程,筆者不才,整了一下午才弄出失敗帶截圖,主要是對conftest.py
的設計編寫,示例程式碼如下:
# -*- coding: utf-8 -*-
"""
@Time : 2022/12/10 18:13
@Auth : 軟體測試君
@File :conftest.py
@IDE :PyCharm
@Motto:ABC(Always Be Coding)
"""
import pytest
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
driver = None
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item):
pytest_html = item.config.pluginmanager.getplugin('html')
outcome = yield
report = outcome.get_result()
extra = getattr(report, 'extra', [])
if report.when == 'call' or report.when == "setup":
xfail = hasattr(report, 'wasxfail')
if (report.skipped and xfail) or (report.failed and not xfail):
file_name = report.nodeid.replace("::", "_") + ".png"
screen_img = _capture_screenshot()
if file_name:
html = '<div><img src="data:image/png;base64,%s" alt="screenshot" style="width:600px;height:300px;" ' \
'οnclick="window.open(this.src)" align="right"/></div>' % screen_img
extra.append(pytest_html.extras.html(html))
report.extra = extra
@pytest.fixture(scope='session')
def browser():
global driver
if driver is None:
driver = webdriver.Chrome(ChromeDriverManager().install())
driver.maximize_window()
yield driver
driver.quit()
return driver
def _capture_screenshot():
"""截圖"""
return driver.get_screenshot_as_base64()
7、執行指令碼
主要用於呼叫測試用例指令碼,示例程式碼如下:
# -*- coding: utf-8 -*-
"""
@Time : 2022/12/10 18:04
@Auth : 軟體測試君
@File :RunTestCase.py
@IDE :PyCharm
@Motto:ABC(Always Be Coding)
"""
import sys
import pytest
from config.conf import ROOT_DIR, HTML_NAME
def main():
if ROOT_DIR not in sys.path:
sys.path.append(ROOT_DIR)
# 執行用例
args = ['--html=' + './report/' + HTML_NAME]
pytest.main(args)
if __name__ == '__main__':
main()
8、測試效果
用例執行效果:
測試報告:
總結
其實寫框架並不難,掌握核心思路,實現起來就會變得容易很多,與語言無關哦(因為我是Java黨
)。
關於API
及很多細節部分,沒做詳細處理和封裝,這裡筆者僅僅是提供思路,感興趣的同學,可自行去嘗試進行進一步擴充套件,如想要原始碼的同學可以文末留言或者加我好友領取哦。