100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > UI自动化框架 基于selenium+pytest和PO分层思想

UI自动化框架 基于selenium+pytest和PO分层思想

时间:2020-12-27 02:13:55

相关推荐

UI自动化框架 基于selenium+pytest和PO分层思想

最近在编写UI自动化框架,现在将一些碎片化东西进行梳理,便于记忆

同时,为了方便于各个模块的独立管理,以及秉承高复用,低耦合的思想,这里是根据PO模型编写,同时将所有的模块进行了独立,页面和元素,以及用例和操作

框架用到的所有分层,梳理一下每个包的用途

.pytest_cache 这个是使用pytest框架系统默认导入的

commom公共管理方法

common_handle: 这里可以理解为base_page,这里对selenium里的方法进行了二次封装,同时对于一些操作,会在代码段里做注释,就不过多叙述

# encoding: utf-8"""@author:辉辉@email: 372148872@@Wechat: 不要静音@time: /12/14 15:15"""from selenium.webdriver.remote.webdriver import WebDriverfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.support.wait import WebDriverWaitfrom JPT_UITEST.Config.path_name import PathNamefrom mon import timefrom mon.log_handle import loggerclass BasePage:# 初始化,传入一个driverdef __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))raisedef error_screenshots(self, name):"""保存截图:param name:根据被调用传入的名字,生成png的图片:return:"""try:file_path = PathName.screenshots_pathtimes = 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))raisedef 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)raiseelse:return eledef 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)raisedef 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)raisedef 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.textlogger.info('获取文本成功{}'.format(text))return textexcept: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 valueexcept: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))raisedef 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对象

同时,这里二次封装的主要思想为:要做什么->日志打印->等待元素->判断->截图这样可以清晰的知道,那个地方出了问题,有日志,有截图

mon_mysql:

这里是预留封装mysql的文件,因为暂时用不到,就没使用

3.log_handle:

这里封装的是log日志模块

# encoding: utf-8"""@author:辉辉@email: 372148872@@Wechat: 不要静音@time: /11/23 17:19"""import loggingimport osfrom mon_data import Contextfrom JPT_UITEST.Config.path_name import PathNamelog_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 = loggerlog.warning('测试1')

这里是通用模板,不多说

4. time.py

这里是生成时间

# encoding: utf-8"""@author:辉辉@email: 372148872@@Wechat: 不要静音@time: /12/15 14:56"""import datetimedef time_sj():sj = datetime.datetime.now().strftime('%m-%d-%H-%M-%S')return sj

Config 这里我存放的是路径,以及公共操作里会用到的数据

common_data.py

这里就放了一些关于日志的等级和level

# encoding: utf-8"""@author:辉辉@email: 372148872@@Wechat: 不要静音@time: /12/15 13:47"""class Context:log_name = 'Ui自动化.txt'level = 'DEBUG'

path_name.py

这里是存放了所有会用到的存放路径,以及读取数据路径

# encoding: utf-8"""@author:辉辉@email: 372148872@@Wechat: 不要静音@time: /12/15 13:47"""import osclass PathName:# 初始路径F:\jintie\JPT_UITESTdir_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: 372148872@@Wechat: 不要静音@time: /12/14 15:21"""class GlobalData:# 域名host = ''# urlBackGround = host + '/login'

Test_data:

test_data.py: 存放的测试数据

数据分离的好处在于,如果数据发生改变,我只需要在这个文件中将数据做修改即可,不需要去做其他的代码改动

# encoding: utf-8"""@author:辉辉@email: 372148872@@Wechat: 不要静音@time: /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: 372148872@@Wechat: 不要静音@time: /12/14 15:13"""“”“涉及了实际项目,所以部分数据做脱敏处理”“”from mon.by import Byclass 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_msgerror_msg = (By.XPATH, '//div[@class="ant-form-explain"]')

这里是首页页面所有的元素定位!这样做的好处为,后续如果元素发生了改变,只需要去修改元素即可,不需要对其他地方做出修改!同时,改了这里,所有用到此元素的地方,都会发生改变

PageObject: 这里是所有关于页面操作的封装

background_home.py是关于后台首页的操作

# encoding: utf-8"""@author:辉辉@email: 372148872@@Wechat: 不要静音@time: /12/14 14:49"""import timefrom JPT_UITEST.PageLocation.background_home_loc import BackGroundLoc as BLCfrom mon_handle import BasePageclass 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: 372148872@@Wechat: 不要静音@time: /12/23 14:26"""from JPT_UITEST.data.Global_Data.global_data import GlobalData as GDfrom mon.log_handle import loggerimport pytestfrom 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 driverdriver.quit()

test_login.py 测试用例

# encoding: utf-8"""@author:辉辉@email: 372148872@@Wechat: 不要静音@time: /12/14 14:44"""import timeimport pytestfrom JPT_UITEST.PageObject.background_home import PlatformLoginfrom JPT_UITEST.PageObject.jinpantao_home import JinpantaoHomefrom mon.log_handle import loggerfrom JPT_UITEST.data.Test_Data.test_data import TestData as TDdata = 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: 372148872@@Wechat: 不要静音@time: /12/17 20:13"""import datetimeimport osimport pytestfrom JPT_UITEST.Config.path_name import PathNamefrom mon.log_handle import loggerdef 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测试用例!

到这里,就是一个完整的框架,剩下的就是根据项目的不同,去做一些定制化的操作!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。