POM即Page-Object-Module,是基於頁面對象的自動化測試設計模式,基於該模式設計的自動化框架,直觀的把各頁面元素從代碼邏輯中剝離出來,當系統迭代,頁面元素發生更改時,只需要對單獨剝離出來的頁面元素模塊進行更改,而當業務邏輯更改時更改對應的邏輯模塊,保證了頁面元素與邏輯代碼的復用性,減少了代碼的冗余,符和面向對象的程序設計思想。
在工作中項目往往需求變更較大,版本迭代周期短,基於POM模式設計的UI自動化能夠盡可能的提高測試代碼的復用性,因此重構了相關測試代碼,使之更能滿足業務需求。
一、項目工程目錄
該自動化框架實現基於Python+Selenium+Pytest,引入單例設計思想;
1.Base_Action主要存放基礎操作模塊,實現最基礎的操作封裝;
1.1 base.py模塊中定義了BaseAction類,主要封裝driver的初始化操作,以及單例設計的實現,UI自動化中的單例設計能夠有效解決瀏覽器多開后過高占用測試計算機內存問題;
func_setDriver :: 實現初始化瀏覽器操作,使用對應瀏覽器驅動打開一個瀏覽器,在接下來的測試用例模塊中,每個測試模塊測試前都需要調用該方法;
func_setDriverNone :: 清空初始化后的瀏覽器數據,每個測試模塊測試結束后都需要執行該方法,為下一測試模塊重新初始化瀏覽器做准備;
func_quitDriver :: 進行關閉瀏覽器操作,每個測試模塊測試結束后都需要調用,關閉瀏覽器;
func_findElement :: 定位頁面元素的方法封裝,默認設置30s超時,此方法供接下來的POM文件夾中相關頁面類調用實現對應的頁面元素定位與操作;
根據業務,該BaseAction下可繼續添加相關的方法,實現對常用基礎操作的封裝供其余模塊調用;
class BaseAction(object): _driver = None @classmethod def setDriver(cls): if cls._driver is None: cls._driver = webdriver.Safari() cls._driver.maximize_window() cls._driver.implicitly_wait(5) return cls._driver @classmethod def setDriverNone(cls): if cls._driver : cls._driver = None return cls._driver @classmethod def quiteDriver(cls): return cls.setDriver().quit() @classmethod def findElement(cls,driver,by,locator,outTime=30): try: print('[Info:Starting find the element "{}" by "{}"!]'.format(locator,by)) element = wd(driver, outTime).until(lambda x : x.find_element(by, locator)) except TimeoutException as t: print('error: found "{}" timeout!'.format(locator),t) except NoSuchWindowException as e: print('error: no such "{}"'.format(locator),e) except Exception as e: raise e else: print('[Info:Had found the element "{}" by "{}"!]'.format(locator,by)) return element
1.2 get_Params.py中定義了getParams方法,主要實現從yaml文件中讀取數據,存入datalist列表中,該測試框架的所有測試數據,以及頁面元素數據都以yaml文件格式統一集中存儲方便后期維護;
def getParams(fileName): dataList = [] try: with open(fileName, "r", encoding="UTF-8") as f: conText = yaml.load_all(f, Loader=yaml.FullLoader) for i in conText: dataList.append(i) except Exception as e: print('獲取{}數據失敗!'.format(fileName)) raise e else: return dataList
1.3 get_ScreenShot.py中getScreenShot方法主要是獲取錯誤截圖並存儲在對應文件夾中,在框架的測試用例模塊中都有調用,實現測試失敗時對瀏覽器進行截圖並保存;
2.Page_Elements 主要統一集中存放各個頁面的元素,方便后期維護,每個頁面作為一個模塊獨立存儲,供框架POM中對應頁面類調用實現頁面元素的定位以及操作;
finaceChargePage_Elements.yml :: 集中存儲【會員充值管理】頁面的對應頁面元素
homePage_Elements.yml :: 集中存儲后台首頁頁面的對應頁面元素
loginPage_Elements.yml :: 集中存儲登錄頁面的對應頁面元素
要測試的其他頁面按此方法在該文件夾下添加即可。
#yaml測試數據 #財務充值頁面元素對象 inputname_xpath : //*[@id="app"]/div/div[2]/section/div/div[2]/div[2]/div[1]/div[1]/div/div/input searchbutton_xpath : //*[@id="app"]/div/div[2]/section/div/div[2]/div[1]/div[2]/button[2]/span resultlistname_xpath : //*[@id="app"]/div/div[2]/section/div/div[3]/div[2]/div[3]/table/tbody/tr/td[4]/div resultlistphone_xpath : //*[@id="app"]/div/div[2]/section/div/div[3]/div[2]/div[3]/table/tbody/tr/td[6]/div verbbutton_xpath : //*[@id="app"]/div/div[2]/section/div/div[2]/div[1]/div[2]/button[1]/span
- Params 對測試數據進行集中存儲、維護和管理,主要以yml文件進行存儲
financecharge_Params.yml :: 主要存儲用於測試會員充值管理的測試數據;
login_Params.yml :: 主要存儲用於登錄業務的相關測試用例的測試數據;
同樣該文件夾下的測試數據需要根據測試用例的編寫進行同步維護。
#登錄的url url: http://dv.democeshi.com/web_admin/#/login --- #用戶登錄測試數據 - !!python/tuple - ceshi001 - 1111111a - !!python/tuple - ceshi002 - 111111a - !!python/tuple - ceshi003 - 111111b - !!python/tuple - '' - 1111111b - !!python/tuple - ceshi001 - '' - !!python/tuple - '' - '' --- #用戶正常登錄的賬戶,用於其它業務的測試 - !!python/tuple - ceshi001 - 1111111a
- POM 是存儲頁面基類的文件夾,把每個頁面定義成一個模塊,每個模塊定義該頁面類
accountCharge_page.py :: 會員充值管理頁面元素對象的操作封裝
class AccountCharge(object): def __init__(self): self.driver = BaseAction.setDriver() self.findElement = BaseAction.findElement def searchName(self,name): try: searchNameElement = self.findElement(self.driver, By.XPATH, elements['inputname_xpath']) except Exception as e: loging.logger.error('獲取【會員搜索】輸入框元素失敗!') raise e else: return searchNameElement.send_keys(name) def searchPhone(self,phone): try: searchPhoneElement = self.findElement(self.driver, By.XPATH, elements['inputname_xpath']) except Exception as e: loging.logger.error('獲取【會員搜索】輸入框元素失敗!') raise e else: return searchPhoneElement.send_keys(phone) def searchButtonClick(self): try: searchButtonElement = self.findElement(self.driver, By.XPATH, elements['searchbutton_xpath']) except Exception as e: loging.logger.error('獲取【查詢】按鈕元素失敗!') raise e else: return searchButtonElement.click() @property def resultListNameText(self): try: resultListNameElement = self.findElement(self.driver, By.XPATH, elements['resultlistname_xpath']) except Exception as e: loging.logger.error('獲取結果列表用戶名元素失敗!') raise e else: return resultListNameElement.text @property def resultListPhoneText(self): try: resultListPhoneElement = self.findElement(self.driver, By.XPATH, elements['resultlistphone_xpath']) except Exception as e: loging.logger.error('獲取結果列表用戶電話元素失敗!') raise e else: return resultListPhoneElement.text def verbButtonClick(self): try: verbButtonElement = self.findElement(self.driver, By.XPATH, elements['verbbutton_xpath']) except Exception as e: loging.logger.error('獲取【重置】按鈕元素失敗!') raise e else: return verbButtonElement.click()
home_page.py :: 后台首頁頁面元素對象的操作封裝
class HomePage(object): def __init__(self): self.driver = BaseAction.setDriver() self.findElement = BaseAction.findElement def loginImageClick(self): try: loginImageElement = self.findElement(self.driver, By.XPATH, elements['loginimage_xpath']) except Exception as e: loging.logger.error('獲取首頁登錄用戶頭像失敗!') raise e else: return loginImageElement.click() def quitLoginClick(self): try: quitLoginElement = self.findElement(self.driver, By.XPATH, elements['quitlogin_xpath']) except Exception as e: loging.logger.error('獲取退出登錄元素失敗!') raise e else: return quitLoginElement.click() def indexFinanceClick(self): try: indexFinanceElement = self.findElement(self.driver, By.XPATH, elements['indexfinance_xpath']) except Exception as e: loging.logger.error('獲取首頁【財務】元素失敗!') raise e else: return indexFinanceElement.click() def indexChargeBusinessClick(self): try: indexChargeBusinessElement = self.findElement(self.driver, By.XPATH, elements['indexcharge_xpath']) except Exception as e: loging.logger.error('獲取財務下拉列表中【充值業務】元素失敗!') raise e else: return indexChargeBusinessElement.click() def indexChargeAccountClick(self): try: indexChargeAccountElement = self.findElement(self.driver, By.XPATH, elements['indexchargeaccount_xpath']) except Exception as e: loging.logger.error('獲取首頁列表中【會員充值管理】元素失敗!') raise e else: return indexChargeAccountElement.click()
login_page.py :: 登錄頁面元素對象的操作封裝
class LoginPage(object): def __init__(self): self.driver = BaseAction.setDriver() self.findElement = BaseAction.findElement def userNameInput(self,username): try: self.usernameElement = self.findElement(self.driver, By.XPATH, elements['username_xpath']) except Exception as e: loging.logger.error('獲取username對象異常') raise e else: return self.usernameElement.send_keys(username) def passwordInput(self,password): try: self.passwordElement = self.findElement(self.driver, By.XPATH, elements['password_xpath']) except Exception as e: loging.logger.error('獲取password對象異常') raise e else: return self.passwordElement.send_keys(password) def submitButtonClick(self): try: self.submitButtonElement = self.findElement(self.driver, By.XPATH, elements['submitbutton_xpath']) except Exception as e: loging.logger.error('獲取submit對象異常') raise e else: return self.submitButtonElement.click() @property def loginWrongAccountText(self): try: self.wrongAccountElement = self.findElement(self.driver, By.XPATH, elements['loginwrongaccount_xpath']) except Exception as e: loging.logger.error('獲取登錄失敗提示元素失敗!') raise e else: return self.wrongAccountElement.text @property def loginUsernameNoneText(self): try: self.usernameNoneElement = self.findElement(self.driver, By.XPATH, elements['loginusernamenone_xpath']) except Exception as e: loging.logger.error('獲取登錄失敗提示元素失敗!') raise e else: return self.usernameNoneElement.text @property def loginPasswordNoneText(self): try: self.passwordNoneElement = self.findElement(self.driver, By.XPATH, elements['loginpasswordnone_xpath']) except Exception as e: loging.logger.error('獲取登錄失敗提示元素失敗!') raise e else: return self.passwordNoneElement.text @property def loginAccountNoneText(self): try: self.accountNontElement = self.findElement(self.driver, By.XPATH, elements['loginaccountnone_xpath']) except Exception as e: loging.logger.error('獲取到登錄失敗提示元素失敗!') raise e else: return self.accountNontElement.text def userNameInputClear(self): try: self.usernameElement = self.findElement(self.driver, By.XPATH, elements['username_xpath']) except Exception as e: loging.logger.error('獲取username對象異常') raise e else: return self.usernameElement.clear() def passwordInputClear(self): try: self.passwordElement = self.findElement(self.driver, By.XPATH, elements['password_xpath']) except Exception as e: loging.logger.error('獲取password對象異常') raise e else: return self.passwordElement.clear()
同樣該POM中需要把測試的每個頁面元素對象做對應的封裝,供business調用,完成業務邏輯的代碼實現
現總結一下前面介紹的幾個模塊的數據流:
在整個框架中,POM承擔起了業務邏輯與頁面基礎操作的橋梁,直接對接下來的Business中的業務模塊服務
- Business 主要集中維護整個被測試系統的業務操作邏輯
5.1 例:loginAc.py :: 定義相關的登錄操作
class LoginAction(BaseAction): def __init__(self,url): self.url = url self.driver = BaseAction.setDriver() self.driver.get(url) @classmethod def loginWrongAccount(cls): failText = LoginPage().loginWrongAccountText loging.logger.info('用錯誤的登錄帳號登錄_獲取到的失敗提示:{}'.format(failText)) return failText @classmethod def loginUsernameNone(cls): failText = LoginPage().loginUsernameNoneText loging.logger.info('用戶名為空進行登錄_獲取到的失敗提示:{}'.format(failText)) return failText @classmethod def loginPasswordNone(cls): failText = LoginPage().loginPasswordNoneText loging.logger.info('登錄密碼為空時登錄_獲取到的失敗提示:{}'.format(failText)) return failText @classmethod def loginAccountNone(cls): failText = LoginPage().loginAccountNoneText loging.logger.info('登錄帳號為空時_獲取到的失敗提示為:{}'.format(failText)) return failText def login(self,username,password): # 在登錄頁面輸入用戶名 LoginPage().userNameInput(username) loging.logger.info('在登錄頁面用戶名輸入框輸入:{}'.format(username)) # 在登錄頁面輸入登錄密碼 LoginPage().passwordInput(password) loging.logger.info('在登錄頁面密碼輸入框中輸入:{}'.format(password)) # 在登錄頁面點擊登錄按鈕 LoginPage().submitButtonClick() loging.logger.info('點擊登錄按鈕') time.sleep(3) currUrl = self.driver.current_url if currUrl == self.url and username != '' and password != '': return self.loginWrongAccount() elif currUrl == self.url and username == '' and password != '': return self.loginUsernameNone() elif currUrl == self.url and username != '' and password == '': return self.loginPasswordNone() elif currUrl == self.url and username == '' and password == '': return self.loginAccountNone() else: return currUrl, self.driver
func_login :: 通過傳入username/password,實現登錄功能,其中username/password通過接下來要介紹的Test_Case從Params中獲取,以數據驅動的形式給變量傳入測試參數。
func_loginWrongAccount :: 定義了一個用錯誤帳號登錄后獲取實際彈框文本的方法;
func_loginUsernameNone :: 定義了一個當用戶名為空進行登錄后獲取實際彈框文本的方法;
func_loginPasswordNone :: 定義了一個當登錄密碼為空進行登錄后獲取實際彈框文本的方法;
func_loginAccountNone :: 定義了一個當登錄賬戶為空進行登錄后獲取實際彈框文本的方法;
5.2 viewAc.py :: 該模塊集中處理頁面的跳轉,在該框架下,測試用例的執行都是以模塊為單位,一個模塊定義一個頁面的測試,每個模塊的測試都是以用戶登錄成功進入首頁開始,通過調用viewAc.py中的方法,跳轉到要測試的頁面;
def go_to_financeCharge(): # 在登錄后的首頁點擊導航欄的【財務】 HomePage().indexFinanceClick() loging.logger.info('點擊首頁導航欄【財務】') time.sleep(1) # 在財務下拉列表中點擊【充值業務】 HomePage().indexChargeBusinessClick() loging.logger.info('點擊財務下拉列表中的【充值業務】') time.sleep(1) # 點擊【會員充值管理】 HomePage().indexChargeAccountClick() loging.logger.info('點擊列表中【會員充值管理】') time.sleep(2)
每個測試模塊的執行流程:

- Test_Case 集中維護管理測試用例;
6.1 例:test_FinanceCharge.py :: 后台財務會員充值管理頁面的測試用例
# 該模塊所有測試用例執行的前置條件和后置條件 @pytest.fixture(scope='module', autouse=True) def module(): # 登錄后台系統 LoginAction(url).login(username=loginParams[0][0], password=loginParams[0][1]) # 點擊首頁導航欄的【財務】_【充值業務】_【會員充值管理】 go_to_financeCharge() yield # 執行完模塊所有測試用例后退出瀏覽器 BaseAction.quiteDriver() time.sleep(1) # 執行完模塊所有測試用例后退出登錄 BaseAction.setDriverNone() time.sleep(2) @pytest.fixture() def verbInput(): yield # 每個搜索測試用例執行完后重置清空輸入框 AccountCharge().verbButtonClick() # 測試按用戶名搜索 def test_searchname(verbInput,name =testDatas[0]['name']): # 1.在充值管理頁面用戶名搜索輸入框輸入要搜索人姓名並搜索 FinanceChargeAc.searchName(name) # 2.獲取按姓名搜索的結果列表 result = AccountCharge().resultListNameText assert name == result time.sleep(2) # 測試按手機號搜索 def test_searchphone(verbInput,phone=testDatas[0]['phone']): # 1.在充值管理頁面用戶名搜索輸入框中輸入要搜索的手機號並搜索 FinanceChargeAc.searchPhone(phone) # 2.獲取按手機搜索的結果列表 result = AccountCharge().resultListPhoneText assert phone == result time.sleep(2)
在該模塊中通過@pytest.fixture分別定義了module( )和verbInput( )兩個固件,作用域分別為模塊級別和函數級別,函數中通過yield設置整個模塊所有測試用例和單條測試用例執行的setUp和tearDown,每條測試用例都會調用Business中對應的業務模塊實現業務的操作,而測試數據則通過Base_Action.get_Params來獲取Params中yaml格式的測試數據,作為參數傳入對應函數,通過assert來對預期與實際結果進行斷言,完成測試。
當然在測試用例中可以通過@pytest.mark.parametrize( )實現數據驅動來完成測試;
例如,下面登錄的測試用例中test_login( )就實現了數據驅動的測試
@pytest.fixture(scope='module', autouse=True) def module(): yield # 執行完模塊所有測試用例后退出瀏覽器 BaseAction.quiteDriver() time.sleep(1) # 退出瀏覽器后重置driver BaseAction.setDriverNone() time.sleep(2) @pytest.fixture(autouse=True) def clearInput(): yield # 每條登錄測試執行后清空用戶名和密碼輸入框 LoginPage().userNameInputClear() LoginPage().passwordInputClear() time.sleep(2) @pytest.mark.parametrize('username,password', params) def test_login(username,password): runLogin = LoginAction(url).login(username,password) if username == 'ceshi001' and password == '1111111a': loging.logger.info('正在使用正確的用戶名username:{}和正確的密碼password:{}進行登錄'.format(username,password)) excUrl = 'http://dev.chananchor.com/web_admin/#/dashboard' try: assert runLogin[0] == excUrl except AssertionError as e: getScreenShot() raise e else: QuitLoginAc.quitLogin() time.sleep(3) elif username == '' and password != '': loging.logger.info('用戶名為空,登錄密碼password:{}進行登錄'.format(password)) failText = '請輸入登錄賬號' try: assert runLogin == failText time.sleep(1) except AssertionError as e: getScreenShot() raise e elif username != '' and password == '': loging.logger.info('用戶名username:{},登錄密碼為空進行登錄'.format(username)) failText = '請輸入登錄密碼' try: assert runLogin == failText time.sleep(1) except AssertionError as e: getScreenShot() raise e elif username == '' and password == '': loging.logger.info('用戶名為空,登錄密碼為空進行登錄') failText = '請輸入登錄賬號' try: assert runLogin == failText time.sleep(1) except AssertionError as e: getScreenShot() raise e else: loging.logger.info('用錯誤的用戶名username:{}與密碼password:{}進行登錄'.format(username,password)) failText = '用戶未注冊或密碼錯誤。' try: assert runLogin == failText time.sleep(1) except AssertionError as e: getScreenShot() raise e
登錄的測試數據
#用戶登錄測試數據 - !!python/tuple - ceshi001 - 1111111a - !!python/tuple - ceshi002 - 111111a - !!python/tuple - ceshi003 - 111111b - !!python/tuple - '' - 1111111b - !!python/tuple - ceshi001 - '' - !!python/tuple - '' - ''
- 測試報告的生成
測試報告的生成直接使用pytest框架的html測試報告,通過run.py模塊來執行測試,自動生成報告,存入Report文件夾中
import pytest import time from get_path import getPath from send_mail import sendEmailAttached,sendEmailHtml def runCase(): currtime = time.strftime('%Y-%m-%d') filePath = getPath() reportName = filePath+r'/Report/report{}.html'.format(currtime) pytest.main( [ '--setup-show', '-v', '-s', '--html={}'.format(reportName), 'Test_Case/' ] ) time.sleep(2) sendEmailAttached()
生成的測試報告,當然也可以通過插件來拓展測試報告,根據實際業務需求;
- 測試結束后通過郵件發送測試報告
send_mail.py :: 主要實現郵件發送功能
loging = Logger(__name__, CmdLevel=logging.INFO, FileLevel=logging.INFO) filePath = getPath() currtime = time.strftime('%Y-%m-%d') reportName = filePath+r'/Report/report{}.html'.format(currtime) def sendEmailHtml(): mail_host = "smtp.qq.com" mail_user = "這里填寫郵件發送者" mail_pass = "這里填寫授權碼" sender = '發送者郵箱' receivers = ['郵件接收者','通過列表形式可添加多個'] def readReport(): try: with open(reportName,'rb') as f: msg = f.read() except Exception as e: loging.logger.error('發送郵件模塊讀取{}失敗'.format(reportName)) raise e else: return msg message = MIMEText(readReport(),'html', 'UTF-8') message['From'] = Header('UI自動化測試報告','utf-8') message['To'] = Header('smilepassed@163.com','utf-8') try: smtpObj = smtplib.SMTP() smtpObj.connect(mail_host,25) smtpObj.login(mail_user,mail_pass) smtpObj.sendmail(sender, receivers, message.as_string()) loging.logger.info('郵件發送成功,發送地址:{}'.format(receivers)) except smtplib.SMTPException as e: loging.logger.error('郵件發送失敗!請檢查配置參數') raise e def sendEmailAttached(): mail_host = "smtp.qq.com" mail_user = "這里填寫郵件發送者" mail_pass = "這里填寫授權碼" sender = '發送者郵箱' receivers = ['郵件接收者','通過列表形式可添加多個'] message = MIMEMultipart() message['From'] = Header('UI自動化測試報告','utf-8') message['To'] = Header('smilepassed@163.com','utf-8') subject = 'UI自動化測試報告' message['Subject'] = Header(subject, 'utf-8') message.attach(MIMEText('這是UI自動化測試報告,詳情請看附件!','plain','utf-8')) def readReport(): try: with open(reportName,'rb') as f: msg = f.read() except Exception as e: loging.logger.error('發送郵件模塊讀取{}失敗'.format(reportName)) raise e else: return msg att1 = MIMEText(readReport(),'base64','utf-8') att1["Content-Type"] = 'application/octet-stream' att1.add_header("Content-Disposition", "attachment", filename = ("gbk","","UI自動化測試報告.html")) message.attach(att1) try: smtpObj = smtplib.SMTP() smtpObj.connect(mail_host,25) smtpObj.login(mail_user,mail_pass) smtpObj.sendmail(sender, receivers, message.as_string()) loging.logger.info('郵件發送成功,發送地址:{}'.format(receivers)) except smtplib.SMTPException as e: loging.logger.error('郵件發送失敗!請檢查配置參數') raise e
該發送郵件模塊中,定義了sendEmailHtml() 和sendEmailAttached() 兩個方法,其中sendEmailHtml() 方法為直接發送html報告,sendEmailAttached() 方法為以報告附件的形式發送報告,在run.py中兩種方法任選其一,可完成報告的發送。
- 日志模塊的使用
框架中log.py為日志模塊,主要實現日志的記錄輸出功能,引用該模塊后,通過: loging.logger.info/error ( )來實現日志輸出功能,輸出的日志保存到Log文件夾中
輸出的日志,詳細記錄了測試執行過程中的所有操作,以及輸入的測試數據,同時也會記錄拋出的異常,方便問題的定位;
log.py模塊為日志生成模塊,主要功能函數實現如下
class Logger(object): def __init__(self, logger, CmdLevel=logging.DEBUG, FileLevel=logging.DEBUG): self.logger = logging.getLogger(logger) self.logger.setLevel(logging.INFO) fmt = logging.Formatter('%(asctime)s - %(filename)s:[%(lineno)s] - [%(levelname)s] - %(message)s') currTime = time.strftime("%Y-%m-%d") self.logFileName = filePath+r'/Log/log' + currTime + '.log' fh = logging.FileHandler(self.logFileName) fh.setFormatter(fmt) fh.setLevel(FileLevel) self.logger.addHandler(fh)
綜上所述,整個自動化框架的模塊之間的調用關系如下:
