基于POM模式设计的UI自动化框架
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)
综上所述,整个自动化框架的模块之间的调用关系如下:


浙公网安备 33010602011771号