1. 程式人生 > 其它 >UI自動化框架 基於selenium+pytest和PO分層思想

UI自動化框架 基於selenium+pytest和PO分層思想

技術標籤:pythonseleniumui

最近在編寫UI自動化框架,現在將一些碎片化東西進行梳理,便於記憶
在這裡插入圖片描述
同時,為了方便於各個模組的獨立管理,以及秉承高複用,低耦合的思想,這裡是根據PO模型編寫,同時將所有的模組進行了獨立,頁面和元素,以及用例和操作
框架用到的所有分層,梳理一下每個包的用途

  1.  .pytest_cache 這個是使用pytest框架系統預設匯入的
    
  2.  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測試用例!
到這裡,就是一個完整的框架,剩下的就是根據專案的不同,去做一些定製化的操作!