Python 从无到有搭建WebUI自动化测试框架
目录
前言
1、Python库选择
2、分层设计
3、基础类
浏览器
页面操作
4、公共类
获取框架项目目录的绝对路径
读取excel用例
读取config配置
核心处理工厂
ddt驱动代码
执行并输出报告
打印Log
发送邮件
前言
一个迭代频繁的项目,少不了自动化测试,冒烟与回归全部使用自动化测试来实现,释放我们的人工来测试一些重要的,复杂的工作。节省成本是自动化测试最终目标
Python搭建自动化测试框架是高级测试的标志之一
核心处理工厂是一个骚操作
如果大家都懂了的核心工厂代码,实现了UI自动化框架后,做UI自动化测试时,时间成本比PO模式要低100倍,人力成本可以用初级测试工程师
PO :PageObject
为啥要注释PO呢,有人私下给我留言,意思是我这框架没用PageObject,不行,没写好。
事实上,PO时间和人力成本过于高了。个人追求高效高速低成本的目标,对于这个追求来说,PO它已经跟不上时代了,注定要淘汰。
介如很多朋友问源码,我就将其放在这里,:https://gitee.com/kerici/testUI.git
1、Python库选择
这套框架主要的Python库有 Selenium、unittest、ddt、HTMLTestRunner、win32gui、win32con、openpyxl、configparser、logging、smtplib、os等等
其中Selenium、unittest、ddt、HTMLTestRunner是框架核心模块,Selenium通过html属性得到元素,进而操作元素的方法属性,unittes单元测试模块与ddt数据驱动结合可以实现用例的收集与执行,HTMLTestRunner输出自动化测试报告。
win32gui、win32con操作浏览器一些windows的弹出框,比如上传文件的弹出框
openpyxl库读取excel文件,收集用例
configparser库读取config配置文件,实现常规流程设置可配
logging库输出自动化用例执行日志
smtplib使用email发送测试报告
os获取一些工程里的绝对路径
2、分层设计
目录:
分层设计如目录
所谓分层设计:即是相同属性或类型的代码模块放到同一个目录下,使代码管理,扩展,维护待方便。
basefactory :存放浏览器操作与网页操作的基础代码库与一些浏览器驱动
common:存放执行工厂,收集用例,收集路径,读取配置文件的一代码库
config:存放配置文件
data:存放用例文件
excutetest:存放数据驱动用例代码
library:存放独立三方库,方便自行优化第三方库,引用方便,三方库跟着工程走,不会换环境又要重新下载
result:有三个子目录,log目录,report目录,screenshot目录
3、基础类
浏览器
浏览器操作,有开浏览器,关闭浏览器,切换网页,上传附件等等
个人喜好谷歌浏览器,所以IE与火狐暂没兼容,
1、配置浏览器驱动
首先我们需要下载一个浏览器驱动,下驱动之前先确认下本地浏览器版本
从谷歌浏览器右上角-点击自定义与控制-帮助-关于Google Chrome(G) 找到版本信息,我的是83.0.4103.61
浏览器输入淘宝的谷歌驱动镜像
http://npm.taobao.org/mirrors/chromedriver
找到一个与版本相同或相近的版本驱动
选择与电脑系统对应的驱动下载,一般有linux、mac、win三种。
我的是window系统。所以就下载win32版本,放心64位的系统可以用
下载下来的驱动是个zip,解压出一个chromedriver.exe文件
驱动如何配置,可以参照
https://blog.csdn.net/yinshuilan/article/details/78742728
而我个人喜欢直接丢在当前工程的目录下,放在基础目录 ,方便直接用,随时换。
其实建议按上面链接里的来,换环境方便,我这只是简单粗暴。
2、调试驱动是否有用
在browseroperator.py里写一段代码,
from selenium import webdriverdriver = webdriver.Chrome()driver.get('https://www.baidu.com')
run,结果是自动打开谷歌进入百度了,安排
3、封装浏览器--初始化浏览器
谷歌驱动能用,万事俱备只欠东风,现我们来实现这个东风
代码引用里有from common.getconf import Config
要先实现一个Config类,因为我们要的基础类里要用到一个配置项,这也是为了以后做大做强的需要一个东西,先说一下,具体见【读取config配置】
在config 目录下有一个配置文件,配置文件里配置了浏览器类型,只是为了以后能兼容三种浏览器,目前配置了chrome
首先我们新建名为BrowserOperator的类,初始化配置类的对象,获取到浏览器类型的配置项,得到驱动的目录。初始化用到了os
所以我们要import os
class BrowserOperator(object): def __init__(self): self.conf = Config() self.driver_path = os.path.join(BASEFACTORYDIR, 'chromedriver.exe') self.driver_type = str(self.conf.get('base', 'browser_type')).lower()
然后我们在基础类里实现一个方法def open_url(self, **kwargs): 打开浏览器,方法使用了不定长参数 **kwargs接收传参,所以只能传指定参数或整个字典,
def open_url(self, **kwargs): """ 打开网页 :param url: :return: 返回 webdriver """ try: url = kwargs['locator'] except KeyError: return False, '没有URL参数'
这一段,kwargs里如果有locator,这是用例用例设计的一个参数,它负责传url与元素定位,后面讲excel读取时会说到的。
可以写成url = kwargs.get('locator'),就不需要try了,但要判断url是不是为None,是None还是要return False, ‘没有参数’,两个返回值是全框架设计的返回形式,用一个布尔值确认我的用例执行的成败,后面返回对象或执行日志。至于我写try,个人代码风格。
try: if self.driver_type == 'chrome': #处理chrom弹出的info # chrome_options = webdriver.ChromeOptions() # #option.add_argument('disable-infobars') # chrome_options.add_experimental_option("excludeSwitches", ['enable-automation']) # self.driver = webdriver.Chrome(options=chrome_options, executable_path=self.driver_path) self.driver = webdriver.Chrome(executable_path=self.driver_path) self.driver.maximize_window() self.driver.get(url) elif self.driver_type == 'ie': print('IE 浏览器') else: print('火狐浏览器') except Exception as e: return False, e return True, self.driver
上面一段内容,判断是哪个浏览器,然后指定驱动浏览器打开url。
核心代码
self.dirver = webdriver.Chrome(executable_path=self.driver_path)
此时代码会自动帮我们打开一个浏览器,将浏览的句柄赋值给self.driver。这个句柄是操作网页元素的一个把手,不到浏览器关闭,它不会释放。
然后我们就可以用self.driver.get(url) 打开网站了。
外围包了一个try,就是如果浏览器驱动失败了,我们将返回一个Flase和异常,如果成功了,返回一个True 和浏览器 对象driver,
这里有人会问,driver已经返回了,还把它搞成self.driver 干啥呢,因为这个类后期的关闭浏览器,上传附件等操作要用到这个对象。
哦对了,你们不要问这个把浏览器最大化的方法了,万能固定写法: self.driver.maximize_window()
写完这个,我们来调试一把,在browseroperator.py文件下面写下两代码,然后运行
wd = BrowserOperator()wd.open_url(locator='https://www.qq.com')
对的,locator='https://ww.....' 这就是指定传参
run,结果是打开了qq网站,说明代码已经执行成功了
然后我们改一下代码,
wd = BrowserOperator()isOK, deiver = wd.open_url(locator='https://www.qq.com')time.sleep(5)deiver.find_elements_by_xpath('//*[@id="sougouTxt"]')[0].send_keys('飞人')deiver.find_elements_by_xpath('//*[@id="searchBtn"]')[0].click()
结果他开始搜索飞人了,说明第一步打开浏览器返回操作对象很成功。
4、关闭浏览器
在类下面实现一个方法def close_browser(self, **kwargs):
def close_browser(self, **kwargs): """ 关闭浏览器 :return: """ time.sleep(1) self.driver.quit() time.sleep(2) return True, '关闭浏览器成功'
里面的的睡眠时间很重要的,不然调试时肉眼看不到最后执行结果与否,浏览器就关掉了。
关键代码self.driver.quit(),这个方法退出浏览器,哈哈哈
wd = BrowserOperator()isOK, deiver = wd.open_url(locator='https://www.qq.com')time.sleep(5)deiver.find_elements_by_xpath('//*[@id="sougouTxt"]')[0].send_keys('飞人')deiver.find_elements_by_xpath('//*[@id="searchBtn"]')[0].click()wd.close_browser()
在执行代码后面添加一行wd.close_browser()
run,看着他打开浏览器,搜索飞人,然后关闭浏览器,执行通过。
上传附件,后面写完用例模块时,才能涉及到。网上也有很多上传附件的实现方法。所以就不说了,里面涉及windows的消息机制,很高深,照搬代码即可。
browseroperator.py里所有的代码提供给各位看官,希望多给两个赞,谢谢
import osimport win32guiimport win32conimport timefrom selenium import webdriverfrom common.getconf import Configfrom common.getfiledir import BASEFACTORYDIRfrom pywinauto import applicationclass BrowserOperator(object): def __init__(self): self.conf = Config() self.driver_path = os.path.join(BASEFACTORYDIR, 'chromedriver.exe') self.deriver_type = str(self.conf.get('base', 'browser_type')).lower() def open_url(self, **kwargs): """ 打开网页 :param url: :return: 返回 webdriver """ try: url = kwargs['locator'] except KeyError: return False, '没有URL参数' try: if self.deriver_type == 'chrome': #处理chrom弹出的info # chrome_options = webdriver.ChromeOptions() # #option.add_argument('disable-infobars') # chrome_options.add_experimental_option("excludeSwitches", ['enable-automation']) # self.driver = webdriver.Chrome(options=chrome_options, executable_path=self.driver_path) self.driver = webdriver.Chrome(executable_path=self.driver_path) self.driver.maximize_window() self.driver.get(url) elif self.deriver_type == 'ie': print('IE 浏览器') else: print('火狐浏览器') except Exception as e: return False, e return True, self.driver def close_browser(self, **kwargs): """ 关闭浏览器 :return: """ time.sleep(1) self.driver.quit() time.sleep(2) return True, '关闭浏览器成功' def upload_file(self, **kwargs): """ 上传文件 :param kwargs: :return: """ try: dialog_class = kwargs['type'] file_dir = kwargs['locator'] button_name = kwargs['index'] except KeyError: return True, '没传对话框的标记或没传文件路径,' if self.deriver_type == "chrome": title = "打开" elif self.deriver_type == "firefox": title = "文件上传" elif self.deriver_type == "ie": title = "选择要加载的文件" else: title = "" # 这里根据其它不同浏览器类型来修改 # 找元素 # 一级窗口"#32770","打开" dialog = win32gui.FindWindow(dialog_class, title) if dialog == 0: return False, '传入对话框的class定位器有误' # 向下传递 ComboBoxEx32 = win32gui.FindWindowEx(dialog, 0, "ComboBoxEx32", None) # 二级 comboBox = win32gui.FindWindowEx(ComboBoxEx32, 0, "ComboBox", None) # 三级 # 编辑按钮 edit = win32gui.FindWindowEx(comboBox, 0, 'Edit', None) # 四级 # 打开按钮 button = win32gui.FindWindowEx(dialog, 0, 'Button', button_name) # 二级 if button == 0: return False, '按钮text属性传值有误' # 输入文件的绝对路径,点击“打开”按钮 win32gui.SendMessage(edit, win32con.WM_SETTEXT, None, file_dir) # 发送文件路径 time.sleep(1) win32gui.SendMessage(dialog, win32con.WM_COMMAND, 1, button) # 点击打开按钮 return True, '上传文件成功'
页面操作
浏览器的代码先告一段落,接着得实现页面元素的操作类了
1、引用模块,导包
import osimport timefrom selenium.common.exceptions import NoSuchElementException, TimeoutExceptionfrom selenium.webdriver import Chromefrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.common.by import Byfrom common.getfiledir import SCREENSHOTDIR
os 用来处理截图保存的路径
time处理等待、隐式、显示等待
NoSuchElementException, TimeoutException 前一个隐式等待的异常,后一个显示等的异常
Chrome用来声明driver是Chrome对象,方便driver联想出来方法
expected_conditions 判断元素的16种方法,显示等待用到
WebDriverWait 显示等待
By 可以By.ID,By.NAME等来用来决定元素定位,显示等待中用
SCREENSHOTDIR 截图路径,从getfiledir模块出来,具体见【获取工程绝对路径】
2、初始化类
创建一个类WebdriverOperator类并初始化,因为他需要浏览器driver对象,所以初始化这个对象为私有属性
class WebdriverOperator(object): def __init__(self, driver:Chrome): self.driver = driver
这个也不好调试,先过
2、实现截图
先初始化文件路径与文件名称,文件名使用时间戳命名,保存为png
pic_name = str.split(str(time.time()), '.')[0] + str.split(str(time.time()), '.')[1] + '.png' screent_path = os.path.join(SCREENSHOTDIR, pic_name)
这两行就是实现文件路径的代码
self.driver.get_screenshot_as_file(screent_path)
截屏代码,因为我们是截浏览器的屏,所以使用self.driver对象调用截屏方法,传入路径,它便会自动截屏保存在screent_path文件中,最后返回路径,具休代码如下
def get_screenshot_as_file(self): """ 截屏保存 :return:返回路径 """ pic_name = str.split(str(time.time()), '.')[0] + str.split(str(time.time()), '.')[1] + '.png' screent_path = os.path.join(SCREENSHOTDIR, pic_name) self.driver.get_screenshot_as_file(screent_path) return screent_path
我们来调试一下这个代码
在basefactory目录下面请建一个test.py文件
from basefactory.browseroperator import BrowserOperatorfrom basefactory.webdriveroperator import WebdriverOperatorbo = BrowserOperator()isOK, deiver = bo.open_url(locator='https://www.qq.com')wb = WebdriverOperator(deiver)result = wb.get_screenshot_as_file() #调用截图方法,返回路径打印print(result)
输入这么一段代码。
run,它会打开浏览器,进入qq网站主页,然后打印截图文件目录
找到对应目录 与文件,截图方法调试成功
3、隐式等待
gotosleep(),这个方法不说了,死等。
隐式等待,等待页面元素加载成功便完成等待任务,只需要在用例初始化浏览器之处调用一次,全局可用,其实他只是一行代码,但为了符合框架统一,也封装一下,代码如下:
def web_implicitly_wait(self, **kwargs): """ 隐式等待 :return: type 存时间 """ try: s = kwargs['time'] except KeyError: s = 10 try: self.driver.implicitly_wait(s) except NoSuchElementException: return False, '隐式等待设置失败' return True, '隐式等待设置成功'
其实只有一行代码 self.driver.implicitly_wait(s) 有用的,里面的s是用例传过来的,调试时,只需要传指定time传一个数字,例:time=5,每次页面刷新,程序将等待页面元素加载5秒,5秒后,不管加载成功与否都执行下一行代码,如果2秒有加载完,那么不必等5秒,直接执行下一行代码
切记,隐式等待只需要初始化浏览器调用一次,后面的代码都会隐式等待。
因为我本地网速太好,打开了两个网络视频播放器去调试网页都没法使网页长时间加载,所以只贴上调试代码,结果自己看吧。
在test.py里更新调试代码
from basefactory.browseroperator import BrowserOperatorfrom basefactory.webdriveroperator import WebdriverOperatorbo = BrowserOperator()isOK, deiver = bo.open_url(locator='https://www.baidu.com')wb = WebdriverOperator(deiver)isOK, result = wb.web_implicitly_wait() #设置隐式等待,打印隐式等待的结果print(result)deiver.find_elements_by_xpath('//*[@id="kw"]')[0].send_keys('飞人')deiver.find_elements_by_xpath('//*[@id="su"]')[0].click()deiver.find_elements_by_xpath('//*[@id="rs"]/div')
4、显示等待
隐式等待在页面切换后,加载成功但没展示出来,disable,hide等场景时,也能等待成功。所以不能满足需求。我们需要更强大的等待元素的方法,显示等待,代码如下:
def web_element_wait(self, **kwargs): """ 等待元素可见 :return: """ try: type = kwargs['type'] locator = kwargs['locator'] except KeyError: return False, '未传需要等待元素的定位参数' try: s = kwargs['time'] if s is None: s = 30 except KeyError: s = 30 try: if type == 'id': WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.ID, locator))) elif type == 'name': WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.NAME, locator))) elif type == 'class': WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.CLASS_NAME, locator))) elif type == 'xpath': WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.XPATH, locator))) elif type == 'css': WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.CSS_SELECTOR, locator))) else: return False, '不能识别元素类型[' + type + ']' except TimeoutException: screenshot_path = self.get_screenshot_as_file() return False, '元素[' + locator + ']等待出现失败,已截图[' + screenshot_path + '].' return True, '元素[' + locator + ']等待出现成功'
这两行
type = kwargs['type']
locator = kwargs['locator']
type是用例设计里的locator定位器的类型,有id,name,xpath等主要定位类型,locator定位参数
所以我们调式时,就传两个参数,一个type='' 一个locator=‘’ ,例type='xpath', locator='//*[@id="kw"]'
s = kwargs['time'] 哦,还有这个,传入时间,如果没传,默认等待30秒
核心代码
WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.ID, locator)))
每0.5秒轮寻一次属性id为locator的元素是否可见,可见就跳出等待,返回等等元素出现成功;超过s秒便等待失败,返回元素等待出现失败,截图等信息。
调试一把,在test.py里修改代码如下
from basefactory.browseroperator import BrowserOperatorfrom basefactory.webdriveroperator import WebdriverOperatorbo = BrowserOperator()isOK, deiver = bo.open_url(locator='https://www.baidu.com')wb = WebdriverOperator(deiver)isOK, result = wb.web_element_wait(type='xpath', locator='//*[@id="kw"]', s=0.001)print(result)
设置0.001秒,也能成功,网速太快了,失败的例子等各位来重现了,不想去使用高延迟工具了。
5、输入操作
输入,元素的send_key() 方法,所以在WebdriverOperator类里实现一个element_input方法,代码如下:
def element_input(self, **kwargs): """ :param kwargs: :return: """ try: type = kwargs['type'] locator = kwargs['locator'] text = str(kwargs['input']) except KeyError: return False, '缺少传参' try: index = kwargs['index'] except KeyError: index = 0 try: if type == 'id': elem = self.driver.find_elements_by_id(locator)[index] elif type == 'name': elem = self.driver.find_elements_by_name(locator)[index] elif type == 'class': elem = self.driver.find_elements_by_class_name(locator)[index] elif type == 'xpath': elem = self.driver.find_elements_by_xpath(locator)[index] elif type == 'css': elem = self.driver.find_elements_by_css_selector(locator)[index] else: return False, '不能识别元素类型:[' + type + ']' except Exception as e: screenshot_path = self.get_screenshot_as_file() return False, '获取[' + type + ']元素[' + locator + ']失败,已截图[' + screenshot_path + '].' try: elem.send_keys(text) except Exception: screenshot_path = self.get_screenshot_as_file() return False, '元素['+ locator +']输入['+ text +']失败,已截图[' + screenshot_path + '].' return True, '元素['+ locator +']输入['+ text +']成功'
代码
type = kwargs['type']locator = kwargs['locator']text = str(kwargs['input'])index = kwargs['index']
type是用例设计里的locator定位器的类型,有id,name,xpath等主要类型,locator定位参数,input输入的内容,index是元素的在List里下标,一般页面相同的元素会有很多,我们find元素是得到一个list的结果,需要通过下标来取到自己想到的那个元素。不传默认为第0个。
所以我们调式时,就传四个参数,一个type='' 一个locator=‘’input='' index= ,例type='xpath', locator='//*[@id="kw"]', input='飞人',index=0
这一部分代码
try: if type == 'id': elem = self.driver.find_elements_by_id(locator)[index] elif type == 'name': elem = self.driver.find_elements_by_name(locator)[index] elif type == 'class': elem = self.driver.find_elements_by_class_name(locator)[index] elif type == 'xpath': elem = self.driver.find_elements_by_xpath(locator)[index] elif type == 'css': elem = self.driver.find_elements_by_css_selector(locator)[index] else: return False, '不能识别元素类型:[' + type + ']'except Exception as e: screenshot_path = self.get_screenshot_as_file() return False, '获取[' + type + ']元素[' + locator + ']失败,已截图[' + screenshot_path + '].'
是用来查找页面元素是否存在的,如果存在,将元素找到存在elem变量,继续执行,如果失败,截图,返回False与失败日志
通过type判断,我们使用哪种元素定位方法,这些是基础,不懂的朋友可百度一下寻找元素定位的方法。
输入input的代码:
try: elem.send_keys(text)except Exception: screenshot_path = self.get_screenshot_as_file() return False, '元素['+ locator +']输入['+ text +']失败,已截图[' + screenshot_path + '].'return True, '元素['+ locator +']输入['+ text +']成功'
输入成功,返回True与成功日志,失败,截图并返回False与失败日志
然后我们来调试一把,在test.py文件里更新代码如下:
bo = BrowserOperator()isOK, deiver = bo.open_url(locator='https://www.baidu.com')wb = WebdriverOperator(deiver)isOK, result = wb.web_element_wait(type='xpath', locator='//*[@id="kw"]', s=0.001)print(result)isOK, result = wb.element_input(type='xpath', locator='//*[@id="kw"]', input='飞人', index=0) #元素输入方法print(result)
run,结果打开百度,在编搜索框输入 ‘飞人’,因为还没做点击,先不click百度一下了
再看打印的运行日志
运行成功。
6、优化输入方法
输入方法中的find页面元素是否存在的代码,很多元素操作都需要的。所以再把它分离出来写一个单独的方法find_element,代码如下:
def find_element(self, type, locator, index=None): """ 定位元素 :param type: :param itor: :param index: :return: """ time.sleep(1) #isinstance(self.driver, selenium.webdriver.Chrome.) if index is None: index = 0 type = str.lower(type) try: if type == 'id': elem = self.driver.find_elements_by_id(locator)[index] elif type == 'name': elem = self.driver.find_elements_by_name(locator)[index] elif type == 'class': elem = self.driver.find_elements_by_class_name(locator)[index] elif type == 'xpath': elem = self.driver.find_elements_by_xpath(locator)[index] elif type == 'css': elem = self.driver.find_elements_by_css_selector(locator)[index] else: return False, '不能识别元素类型:[' + type + ']' except Exception as e: screenshot_path = self.get_screenshot_as_file() return False, '获取[' + type + ']元素[' + locator + ']失败,已截图[' + screenshot_path + '].' return True, elem
然后我们的输入方法代码修改为:
def element_input(self, **kwargs): """ :param kwargs: :return: """ try: type = kwargs['type'] locator = kwargs['locator'] text = str(kwargs['input']) except KeyError: return False, '缺少传参' try: index = kwargs['index'] except KeyError: index = 0 isOK, result = self.find_element(type, locator, index) if not isOK: # 元素没找到,返回失败结果 return isOK, result elem = result try: elem.send_keys(text) except Exception: screenshot_path = self.get_screenshot_as_file() return False, '元素['+ locator +']输入['+ text +']失败,已截图[' + screenshot_path + '].' return True, '元素['+ locator +']输入['+ text +']成功'
继续使用用test.py中的代码调试,运行结果与第5点输入操作的运行结果一样,就优化成功了。
7、点击操作
点击操作,元素的click()方法,没有什么好说的,因为他的代码与第5点输入操作一样,只是少了个input参数。所以直接贴代码
def element_click(self, **kwargs): """ :param kwargs: :return: """ try: type = kwargs['type'] locator = kwargs['locator'] except KeyError: return False, '缺少传参' try: index = kwargs['index'] except KeyError: index = 0 isOK, result = self.find_element(type, locator, index) if not isOK: #元素没找到,返回失败结果 return isOK, result elem = result try: elem.click() except Exception: screenshot_path = self.get_screenshot_as_file() return False, '元素['+ locator +']点击失败,已截图[' + screenshot_path + '].' return True, '元素['+ locator +']点击成功'
调试,在test.py里更新代码
bo = BrowserOperator()isOK, deiver = bo.open_url(locator='https://www.baidu.com')wb = WebdriverOperator(deiver)isOK, result = wb.web_element_wait(type='xpath', locator='//*[@id="kw"]', s=0.001)print(result)isOK, result = wb.element_input(type='xpath', locator='//*[@id="kw"]', input='飞人', index=0)print(result)isOK, result = wb.element_click(type='xpath', locator='//*[@id="su"]', index=0) #元素点击方法print(result)
run,运行结果,一切正常
好,基本元素操作的代码解说完了,全部代码如下,不想自己写的可以copy。
还缺少执行JS与一些偏门控件的方法,这些给各位去自个儿写。
import osimport timefrom selenium.common.exceptions import NoSuchElementException, TimeoutExceptionfrom selenium.webdriver import Chromefrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.support.ui import WebDriverWaitfrom selenium.webdriver.common.by import Byfrom common.getfiledir import SCREENSHOTDIRclass WebdriverOperator(object): def __init__(self, driver:Chrome): self.driver = driver def get_screenshot_as_file(self): """ 截屏保存 :return:返回路径 """ pic_name = str.split(str(time.time()), '.')[0] + str.split(str(time.time()), '.')[1] + '.png' screent_path = os.path.join(SCREENSHOTDIR, pic_name) self.driver.get_screenshot_as_file(screent_path) return screent_path def gotosleep(self, **kwargs): time.sleep(3) return True, '等待成功' def web_implicitly_wait(self, **kwargs): """ 隐式等待 :return: type 存时间 """ try: s = kwargs['time'] except KeyError: s = 10 try: self.driver.implicitly_wait(s) except NoSuchElementException: return False, '隐式等待 页面元素未加载完成' return True, '隐式等待 元素加载完成' def web_element_wait(self, **kwargs): """ 等待元素可见 :return: """ try: type = kwargs['type'] locator = kwargs['locator'] except KeyError: return False, '未传需要等待元素的定位参数' try: s = kwargs['time'] if s is None: s = 30 except KeyError: s = 30 try: if type == 'id': WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.ID, locator))) elif type == 'name': WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.NAME, locator))) elif type == 'class': WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.CLASS_NAME, locator))) elif type == 'xpath': WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.XPATH, locator))) elif type == 'css': WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.CSS_SELECTOR, locator))) else: return False, '不能识别元素类型[' + type + ']' except TimeoutException: screenshot_path = self.get_screenshot_as_file() return False, '元素[' + locator + ']等待出现失败,已截图[' + screenshot_path + '].' return True, '元素[' + locator + ']等待出现成功' def find_element(self, type, locator, index=None): """ 定位元素 :param type: :param itor: :param index: :return: """ time.sleep(1) #isinstance(self.driver, selenium.webdriver.Chrome.) if index is None: index = 0 type = str.lower(type) try: if type == 'id': elem = self.driver.find_elements_by_id(locator)[index] elif type == 'name': elem = self.driver.find_elements_by_name(locator)[index] elif type == 'class': elem = self.driver.find_elements_by_class_name(locator)[index] elif type == 'xpath': elem = self.driver.find_elements_by_xpath(locator)[index] elif type == 'css': elem = self.driver.find_elements_by_css_selector(locator)[index] else: return False, '不能识别元素类型:[' + type + ']' except Exception as e: screenshot_path = self.get_screenshot_as_file() return False, '获取[' + type + ']元素[' + locator + ']失败,已截图[' + screenshot_path + '].' return True, elem def element_click(self, **kwargs): """ :param kwargs: :return: """ try: type = kwargs['type'] locator = kwargs['locator'] except KeyError: return False, '缺少传参' try: index = kwargs['index'] except KeyError: index = 0 isOK, result = self.find_element(type, locator, index) if not isOK: #元素没找到,返回失败结果 return isOK, result elem = result try: elem.click() except Exception: screenshot_path = self.get_screenshot_as_file() return False, '元素['+ locator +']点击失败,已截图[' + screenshot_path + '].' return True, '元素['+ locator +']点击成功' def element_input(self, **kwargs): """ :param kwargs: :return: """ try: type = kwargs['type'] locator = kwargs['locator'] text = str(kwargs['input']) except KeyError: return False, '缺少传参' try: index = kwargs['index'] except KeyError: index = 0 isOK, result = self.find_element(type, locator, index) if not isOK: # 元素没找到,返回失败结果 return isOK, result elem = result try: elem.send_keys(text) except Exception: screenshot_path = self.get_screenshot_as_file() return False, '元素['+ locator +']输入['+ text +']失败,已截图[' + screenshot_path + '].' return True, '元素['+ locator +']输入['+ text +']成功'
4、公共类
获取框架项目目录的绝对路径
先在common目录下新增一个,getfiledir.py的文件
这一段没什么多说的,就是获取本框架各目录的绝对路径
import osdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))DATADIR = os.path.join(dir, 'data')CONFDIR = os.path.join(dir, 'config')BASEFACTORYDIR = os.path.join(dir, 'basefactory')RESULTDIR = os.path.join(dir, 'result')LOGDIR = os.path.join(RESULTDIR, 'log')REPORTDIR = os.path.join(RESULTDIR, 'report')SCREENSHOTDIR = os.path.join(RESULTDIR, 'screenshot')CASEDIR = os.path.join(dir, 'excutetest')#print(SCREENSHOTDIR)
我们可以在每行代码下面写一个print()语名来打印目录,调试一把,结果如下,所以目录已成功得到
读取excel用例
先在common目录下新增一个getcase.py文件
1、用例设计与用例格式
首先,我们设计测试用例,格式如下:
保存为一个xlsx格式的excel文档,放在data目录下,这个我们的数据目录,也是存放测试用例的目录,数据驱动的数据,也就是这个目录。
读取用例在框架里存放的格式如下,
这个是基础,看得懂就行,写好了代码一万年不要动,似懂非懂利害了,需要去学习巩固一下Python dict,其他语言的json串
[
{ 整个xlsx文件的用例
"a": [ 整个sheet页的用例
{用例
"A": 2
},
{
"A": 3
}
]
},
{
"b": [
{
"A": 2
},
{
"A": 3
}
]
}
]
2、引用导包,初始化case.xlsx目录
import openpyxl
import os
from common.getfiledir import DATADIR #导入data目录
file = os.path.join(DATADIR, 'case.xlsx') #得到case文件的路径
导入了openpyxl,它只能操作xlsx,支持读写,本人只喜欢读,不喜欢写。
os,初始化用例目录有用到。写死用例文件,如果想灵活多变,可以把用例名字变成传参或变成配置
用例目录
3、初始化ReadCase
class ReadCase(object): def __init__(self): self.sw = openpyxl.load_workbook(file) print(self.sw)
初始化类,顺便读取一下用例文件
调试
在文件里输入,
xlsx = ReadCase()
run,打印出这么一串人类不认识,那么就成功了,他打印了一个内存对象,保存了整个case文件
4、读取单个sheet页用例
实现一个方法readcase, 代码如下
def readcase(self, sh): """ 组合sheet页的数据 :param sh: :return: list,返回组合数据 """ if sh is None: return False, '用例页参数未传' datas = list(sh.rows) if datas == []: return False, '用例[' + sh.title + ']里面是空的' title = [i.value for i in datas[0]] rows = [] sh_dict = {} for i in datas[1:]: data = [v.value for v in i] row = dict(zip(title, data)) try: if str(row['id'])[0] is not '#': row['sheet'] = sh.title rows.append(row) except KeyError: raise e rows.append(row) sh_dict[sh.title] = rows return True, sh_dict
参数传入一个sheet页对象,
然后判空,
if sh is None: return False, '用例页参数未传'
通过列表保存sheet的每一行,判空
datas = list(sh.rows)if datas == []: return False, '用例[' + sh.title + ']里面是空的'
得到第一行为title,为啥呢,看excel文件的格式
title = [i.value for i in datas[0]]
用一个循环得到每一行的用例与title结合成一个字典json串,返回
for i in datas[1:]: data = [v.value for v in i] row = dict(zip(title, data)) try: if str(row['id'])[0] is not '#': row['sheet'] = sh.title rows.append(row) except KeyError: raise e rows.append(row) sh_dict[sh.title] = rowsreturn True, sh_dict
好了,来调试一发,在下面输入代码:
xlsx = ReadCase()for sh in xlsx.sw: isOK, result = xlsx.readcase(sh) print(result)
先看我们用例文件
run,运行结果如下
格式化后的结果,完全是我们需要的样子,成功读取用例。
{
'baidu': [{
'id': 1,
'result': None,
'keyword': '打开网页',
'type': 'url',
'locator': 'https://www.baidu.com',
'index': None,
'input': None,
'check': None,
'time': None,
'sheet': 'baidu'
}, {
'id': 4,
'result': None,
'keyword': '等待元素可见',
'type': 'xpath',
'locator': '//*[@id="kjw"]',
'index': None,
'input': None,
'check': None,
'time': 3,
'sheet': 'baidu'
}, {
'id': 2,
'result': None,
'keyword': '输入',
'type': 'xpath',
'locator': '//*[@id="kw"]',
'index': None,
'input': '飞人',
'check': None,
'time': None,
'sheet': 'baidu'
}]
}
5、读取所有用例
因为我们设计的readcase方法,只需要传入一个sheet页,所以实现一个readallcase的方法,遍历所有sheet页,传给readcase读取所有用例,代码如下:
def readallcase(self): """ 取所有sheet页 :return:list,返回sheet页里的数据 """ sheet_list = [] for sh in self.sw: #遍历sheet, if 'common' != sh.title.split('_')[0] and 'common' != sh.title.split('-')[0] and sh.title[0] is not '#' : #判断是否可用的用例 isOK, result = self.readcase(sh) #传给readcase取用例 if isOK: sheet_list.append(result) #得到结果放到列表,又给用例套了一层sheet页的框 if sheet_list is None: return False, '用例集是空的,请检查用例' return True, sheet_list
这里面有一个判断条件,是公共用例,common开头的用例不读取,注释用例以#号开头的用例,不读取
调试代码
xlsx = ReadCase()isOK, result = xlsx.readallcase()print(result)
运行结果,请看比第4点运行的结果多了一个中括号,表示他已经可以读取所有的用例了
6、读取公共用例
实现一个get_common_case方法,方法很简单,查询是否有传参sheetname的sheet页存在,有的话就读取sheet页的用例,返回,代码如下。
def get_common_case(self, case_name): """ 得到公共用例 :param case_name: :return: """ try: sh = self.sw.get_sheet_by_name(case_name) except KeyError: return False, '未找到公共用例[' + case_name + '],请检查用例' except DeprecationWarning: pass return self.readcase(sh)
要结合核心模块的调用
调试代码如下
xlsx = ReadCase()isOK, result = xlsx.get_common_case('baidu')print(result)
结果如第5点结果一样
读取用例的全部代码如下:
import openpyxlimport osfrom common.getfiledir import DATADIRfile = os.path.join(DATADIR, 'case.xlsx')class ReadCase(object): def __init__(self): self.sw = openpyxl.load_workbook(file) print(self.sw) def openxlsx(self, file): """ 打开文件 :param dir: :return: """ # self.sw = openpyxl.load_workbook(file) #[{"a": [{"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}]},{"a": [5, 3, 2]}, {"a": [10, 4, 6]}] def readallcase(self): """ 取所有sheet页 :return:list,返回sheet页里的数据 """ sheet_list = [] for sh in self.sw: if 'common' != sh.title.split('_')[0] and 'common' != sh.title.split('-')[0] and sh.title[0] is not '#' : isOK, result = self.readcase(sh) if isOK: sheet_list.append(result) if sheet_list is None: return False, '用例集是空的,请检查用例' return True, sheet_list def readcase(self, sh): """ 组合sheet页的数据 :param sh: :return: list,返回组合数据 """ if sh is None: print('sheet页为空') datas = list(sh.rows) if datas == []: return False, '用例[' + sh.title + ']里面是空的' title = [i.value for i in datas[0]] rows = [] sh_dict = {} for i in datas[1:]: data = [v.value for v in i] row = dict(zip(title, data)) try: if str(row['id'])[0] is not '#': row['sheet'] = sh.title rows.append(row) except KeyError: raise e rows.append(row) sh_dict[sh.title] = rows return True, sh_dict def get_common_case(self, case_name): """ 得到公共用例 :param case_name: :return: """ try: sh = self.sw.get_sheet_by_name(case_name) except KeyError: return False, '未找到公共用例[' + case_name + '],请检查用例' except DeprecationWarning: pass return self.readcase(sh)
读取config配置
目录:
Config类没有什么好解析的,暂时是为了封装而封装
具体代码如下:
import osfrom configparser import ConfigParserfrom common.getfiledir import CONFDIRclass Config(ConfigParser): def __init__(self): self.conf_name = os.path.join(CONFDIR, 'base.ini') super().__init__() super().read(self.conf_name, encoding='utf-8') def save_data(self, section, option, value): super().set(section=section, option=option, value=value) super().write(fp=open(self.conf_name, 'w'))
配置文件分析一下
目录
内容
[base]browser_type = chrome# browser_type = IE# browser_type = firefox[LOG]level = INFO
[email]host = smtp.qq.comport = 465user = [email protected] pwd = ********* from_addr = [email protected] to_addr = [email protected]
[Function] 打开网页 = open_url关闭浏览器 = close_browser对话框上传文件 = upload_file点击 = element_click输入 = element_input截图 = get_screenshot_as_file寻找元素 = find_element隐式等待 = web_implicitly_wait等待元素可见 = web_element_wait等待 = gotosleepinput上传文件 = upload_input_file模拟按键上传文件 = upload_endkey_file切换页面 = toggle_page执行JS = execute_js
1、base配置,框架中的基础配置
目前只有浏览器类型
2、LOG配置
配置LOG的输出等级,为INFO,日志模块会讲到
3、email配置
email的一些参数配置,smtp服务器与端口,smtp账密,收发地址等
4、Function配置
两个基础类操作行方法 与 用例参数里的keyword的映射关系,核心处理工厂会用到
核心处理工厂
核心处理工厂为啥核心呢?
各位自动化测试大佬,应该知道PO模型吧,一些常用公共页面,封装到一个类里。之后哪里要用到,哪里就初始化这个类来调用各页面的代码执行。
但每个项目都有这么多页面,每更新一个项目就要写海量代码,这也是UI自动化不如接口自动化的地方,难维护,成本高,
但我这个核心代码,完全不需要用PO模式了,只需要在excel里面写公共用例,即可替代PO模式。
可以说,如果大家都懂了我这段核心代码,实现了UI自动化框架后,做UI自动化时,时间成本就比PO模式要低100倍,人力成本可以用初级测试工程师
上面只是说明核心是重点,但真正核心还是因为他是全框架的总枢纽,后面会细述说来
目录
1、引用模块,导包
from common.getconf import Configfrom common.getcase import ReadCasefrom basefactory.browseroperator import BrowserOperatorfrom basefactory.webdriveroperator import WebdriverOperator
导入了Config,配置文件读取
导入了ReadCase,来初始化用例
导入了BrowserOperator,WebdriverOperator,初始化这个两个类,执行用例
2、初始化工厂类
class Factory(object): def __init__(self): self.con = Config() self.con_fun = dict(self.con.items('Function')) """ 浏览器操作对象 """ self.browser_opr = BrowserOperator() """ 网页操作对象 """ self.webdriver_opr = None
将配置项Function初始化进来
self.con = Config()self.con_fun = dict(self.con.items('Function'))
再初始化一下两个基础类对象,因为WebdriverOperator类要在打开URL后才能初始化,先初始化为None
self.browser_opr = BrowserOperator()
self.webdriver_opr = None
然后我们将初始化webdriver_opr的代码放在一个函数里面,方便后面执行方法里面调用,里面强制转换一下,不然很难联想其对应的方法
def init_webdriver_opr(self, driver): self.webdriver_opr = WebdriverOperator(driver)
下面我们先说如何实现代替PO模型的用例初始化
3、初始化执行用例
初始化执行用例 def init_execute_case(self): 里面的内容:
def init_execute_case(self): print("----------初始化用例----------") xlsx = ReadCase() isOK, result = xlsx.readallcase() if not isOK: print(result) print("----------结束执行----------") exit() all_cases = result excu_cases = [] for cases_dict in all_cases: for key, cases in cases_dict.items(): isOK, result = self.init_common_case(cases) if isOK: cases_dict[key] = result else: cases_dict[key] = cases excu_cases.append(cases_dict) print("----------初始化用例完成----------") return excu_cases
一、读取了excel里所有可执行用例
二、调用用初始化公共用例
4、初始化公共用例
#初始化公共用例 def init_common_case(self, cases): 里面的内容:
def init_common_case(self, cases): """ :param kwargs: :return: """ cases_len = len(cases) index = 0 for case in cases: if case['keyword'] == '调用用例': xlsx = ReadCase() try: case_name = case['locator'] except KeyError: return False, '调用用例没提供用例名,请检查用例' isOK, result = xlsx.get_common_case(case_name) if isOK and type([]) == type(result): isOK, result_1 = self.init_common_case(result) #递归检查公共用例里是否存在调用用例 elif not isOK: return isOK, result list_rows = result[case_name] cases[index: index+1] = list_rows #将公共用例插入到执行用例中去 index += 1 if cases_len == index: return False, '' return True, cases
一、判断是否有‘调用用例’命令,有则取公共用例合并成可执行用例
二、递归取公共用例里是否有‘调用用例’命令,有则继续取公共合并成可执行用例
注意:公共用例不能调用自已,递归死循环
调试这两个方法一起
首先人们得在用例里面写上一个公共用例
再在baidu用例里写一行,调用用例
然后你们看看,这是不是就是PO模型,只需要在excel里面写写就行,完全不需要代码了
我们来调试一下,大工厂类里写下调试代码
fac = Factory()isOK, result = fac.init_execute_case()print(result)
运行结果如下,我们初始化用例后,会把调用用例里被调用例的用例插入到执行用例中来,全部去执行。
下面来讲执行部分的
5、获取执行方法
获取两个基础WebdriverOperator、BrowserOperator的方法,具体代码如下:
def get_base_function(self, function_name): try: function = getattr(self.browser_opr, function_name) except Exception: try: function = getattr(self.webdriver_opr, function_name) except Exception: return False, '未找到注册方法[' + function_name + ']所对应的执行函数,请检查配置文件' return True, function
传入方法名称function_name,通过getattr得到基础类的方法,成功得到方法,返回True,function,没有得到,返回False,日志
6、方法执行
实现一个方法,execute_keyword,统一入口调用两个基础类的操作
先放代码,再讲原理,代码如下:
def execute_keyword(self, **kwargs): """ 工厂函数,用例执行方法的入口 :param kwargs: :return: """ try: keyword = kwargs['keyword'] if keyword is None: return False, '没有keyword,请检查用例' except KeyError: return False, '没有keyword,请检查用例' _isbrowser = False try: function_name = self.con_fun[keyword] except KeyError: return False, '方法Key['+ keyword +']未注册,请检查用例' #获取基础类方法 isOK, result = self.get_base_function(function_name) if isOK: function = result else: return isOK, result #执行基础方法,如打网点页、点击、定位、隐式等待 等 isOK, result = function(**kwargs) #如果是打开网页,是浏览器初始化,需要将返回值传递给另一个基础类 if '打开网页' == keyword and isOK: url = kwargs['locator'] self.init_webdriver_opr(result) return isOK, '网页[' + url + ']打开成功' return isOK, result
原理:先得到用例里的keyword,然后获取到两个基础类里方法,再传入**kwargs调用,执行操作
取到keyword关键字,回顾上面用例excel里的有一个keyword的字段,传入进来,先取这个字段
try: keyword = kwargs['keyword'] if keyword is None: return False, '没有keyword,请检查用例'except KeyError: return False, '没有keyword,请检查用例'
在self.con_fun键值对里取到keyword对应的方法名,具体方法可见配置文件那一节
try: function_name = self.con_fun[keyword]except KeyError: return False, '方法Key['+ keyword +']未注册,请检查用例'
通过get_base_function得到基础类的方法
isOK, result = self.get_base_function(function_name)if isOK: function = resultelse: return isOK, result
执行方法,返回结果
#执行基础方法,如打网点页、点击、定位、隐式等待 等isOK, result = function(**kwargs)
当然,里面有一个特别的,如果方法是打开浏览器,这时就要初始化一下self.webdriver
if '打开网页' == keyword and isOK: url = kwargs['locator'] self.init_webdriver_opr(result) return isOK, '网页[' + url + ']打开成功'最后我们返回执行结果return isOK, result
调试,我们在common里面新增一个test.py文件,在里面输入调试代码
导入Factory,初始化一个工厂对象fac,用fac,调用execute_keyword()方法来执行所有网页操作
from common.factory import Factoryfac = Factory()isOK, result = fac.execute_keyword(keyword='打开网页', locator='http://www.baidu.com')print(result)isOK, result = fac.execute_keyword(keyword='隐式等待', time='30')print(result)isOK, result = fac.execute_keyword(keyword='输入', type='xpath', locator='//*[@id="kw"]',input='飞人乔丹')print(result)idOK, result = fac.execute_keyword(keyword='点击', type='xpath', locator='//*[@id="su"]')print(result)
run,运行结果:
再联合读取excel用例来一起调试
因为
fac.execute_keyword(keyword='点击', type='xpath', locator='//*[@id="su"]')
这样传参其实就等于传了一个字典,所以我们可以将用例里面字典直接传与给execute_keyword()
首先我们整理下excel里的用例
公共用例 common-bai里面是在一个id=kw的输入框中输入‘路飞’
可执行用例baidu,是打开百度网站,输入飞人,然后调用公共用例,再点击搜索的一个模拟搜索的用例
我们来写一下调试代码 ,在test.py里面修改代码
先初始化用例init_exceute_case()
然后解析用例,
for acases in result: #遍历外层list
for key, cases in acases.items(): #遍历中间的dict
for case in cases: #遍历将case取出
用例的层级关系在下面,用上面三行代码解析出case
[
{ 整个xlsx文件的用例
"a": [ 整个sheet页的用例
{用例
"A": 2
},
{
"A": 3
}
]
},
{
"b": [
{
"A": 2
},
{
"A": 3
}
]
}
]
解析完了
调用execute_keyword(**case)
from common.factory import Factoryfac = Factory()isOK, result = fac.init_execute_case()for acases in result: for key, cases in acases.items(): for case in cases: isOK, result = fac.execute_keyword(**case) print(result)
run,运行结果如下
看,在执行用例里面的输入【飞人】成功,然后调用公共用例里面的,执行输入【路飞】成功
最后又回到执行用例里的点击su百度一下成功
核心代码的讲述解束
核心部分全部代码如下:
from common.getconf import Configfrom common.getcase import ReadCasefrom basefactory.browseroperator import BrowserOperatorfrom basefactory.webdriveroperator import WebdriverOperatorclass Factory(object): def __init__(self): self.con = Config() self.con_fun = dict(self.con.items('Function')) """ 浏览器操作对象 """ self.browser_opr = BrowserOperator() """ 网页操作对象 """ self.webdriver_opr = None def init_webdriver_opr(self, driver): self.webdriver_opr = WebdriverOperator(driver) def get_base_function(self, function_name): try: function = getattr(self.browser_opr, function_name) except Exception: try: function = getattr(self.webdriver_opr, function_name) except Exception: return False, '未找到注册方法[' + function_name + ']所对应的执行函数,请检查配置文件' return True, function def execute_keyword(self, **kwargs): """ 工厂函数,用例执行方法的入口 :param kwargs: :return: """ try: keyword = kwargs['keyword'] if keyword is None: return False, '没有keyword,请检查用例' except KeyError: return False, '没有keyword,请检查用例' _isbrowser = False try: function_name = self.con_fun[keyword] except KeyError: return False, '方法Key['+ keyword +']未注册,请检查用例' #获取基础类方法 isOK, result = self.get_base_function(function_name) if isOK: function = result else: return isOK, result #执行基础方法,如打网点页、点击、定位、隐式等待 等 isOK, result = function(**kwargs) #如果是打开网页,是浏览器初始化,需要将返回值传递给另一个基础类 if '打开网页' == keyword and isOK: url = kwargs['locator'] self.init_webdriver_opr(result) return isOK, '网页[' + url + ']打开成功' return isOK, result def init_common_case(self, cases): """ :param kwargs: :return: """ cases_len = len(cases) index = 0 for case in cases: if case['keyword'] == '调用用例': xlsx = ReadCase() try: case_name = case['locator'] except KeyError: return False, '调用用例没提供用例名,请检查用例' isOK, result = xlsx.get_common_case(case_name) if isOK and type([]) == type(result): isOK, result_1 = self.init_common_case(result) #递归检查公共用例里是否存在调用用例 elif not isOK: return isOK, result list_rows = result[case_name] cases[index: index+1] = list_rows #将公共用例插入到执行用例中去 index += 1 if cases_len == index: return False, '' return True, cases # [{"a": [{"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}, {"A": 2}]},{"a": [5, 3, 2]}, {"a": [10, 4, 6]}] def init_execute_case(self): print("----------初始化用例----------") xlsx = ReadCase() isOK, result = xlsx.readallcase() if not isOK: print(result) print("----------结束执行----------") exit() all_cases = result excu_cases = [] for cases_dict in all_cases: for key, cases in cases_dict.items(): isOK, result = self.init_common_case(cases) if isOK: cases_dict[key] = result else: cases_dict[key] = cases excu_cases.append(cases_dict) print("----------初始化用例完成----------") return excu_cases
ddt驱动代码
1、文件目录
执行用例代码目录 excutetest下新增一个test_caserun.py ,先说,这里最好是以test开头定义执行文件
然后library里面放到ddt,数据驱动装饰器
2、导入库
在test_caserun.py里写代码
import unittestfrom common.factory import Factoryfrom common.log import mylogfrom library.ddt import ddt, data
unittest UT测试库,作自动化测试的都知道他是干啥的
Factory,这个框架的核心类
mylog,下一章实现的日志打印
ddt, data,数据驱动装饰器
2、代码分析
@ddtclass Test_caserun(unittest.TestCase): fac = Factory() isOK, excu_cases = fac.init_execute_case() @data(*excu_cases) def test_run(self, acases): for key, cases in acases.items(): mylog.info('\n----------用例【%s】开始----------' % cases[0].get('sheet')) print('\n') for case in cases: isOK, result = self.fac.execute_keyword(**case) if isOK: print(result) mylog.info(result) else: mylog.error(result) raise Exception(result) mylog.info('\n----------用例【%s】结束----------\n' % cases[0].get('sheet')).
定义一个类Test_caserun,用@ddt来装饰一下这个类,不要问这啥原理,一下子说不清楚,按要求来就行
初始化工厂fac = Factory()
得到用例isOK, excu_cases = fac.init_execute_case()
实现一个执行用例的方法,test_run
用@data() 来装饰这个方法,这个装饰器来帮忙遍历excu_cases用例,然后返回给test_run方法的acases,
@data() 的参数可以是元组、列表、字典等格式
data只遍历了第一层,后面的我们代码里面写
for key, cases in acases.items(): mylog.info('\n----------用例【%s】开始----------' % cases[0].get('sheet')) print('\n') for case in cases: isOK, result = self.fac.execute_keyword(**case) if isOK: print(result) mylog.info(result) else: mylog.error(result) raise Exception(result) mylog.info('\n----------用例【%s】结束----------\n' % cases[0].get('sheet'))
这一段就是执行用例,决断执行结果如
理念就是简练代码,一个用例执行代码,调用一个执行入口,执行任意网页操作,返回成功或失败
isOK is True 执行成功打info日志
isOK is False 执行失败打error日志,抛出异常日志
调试代码看下章
执行并输出报告
1、目录
在工程主目录下添加一个文件test_run.py
https://download.csdn.net/download/weixin_40331132/12521680
下载上面链接里的一个文件放到library目录下,因为我的
网上有很多,但因为是我写的用例格式,所以HTMLTestRunner里的代码我改了一些。暂时不会修改这个文件的朋友,可以先下载我的来学习用
2、导包
在test_run里导入
import osimport unittestfrom library.HTMLTestRunnerNew import HTMLTestRunnerfrom common.getfiledir import CASEDIR, REPORTDIR
os来处理路径
unittest来Loadcase的代码文件
HTMLTestRunner来执行用例,输出报告
具体代码如下,这一段网上通用,我也懒懒不细说了直接上
class Test_run(object): def __init__(self): self.suit = unittest.TestSuite() self.load = unittest.TestLoader() self.suit.addTest(self.load.discover(CASEDIR)) self.runner = HTMLTestRunner( stream=open(os.path.join(REPORTDIR, 'report.html'), 'wb'), title='魂尾自动化工厂', description='唯a饭木领', tester='HUNWEI' ) def excute(self): self.runner.run(self.suit)if __name__=="__main__": test_run = Test_run() test_run.excute()
init初始化用例,与报告对象
excute执行
添加工程主执行入口main
直接运行调试,还是在之前的excel用例基础上运行
运行结果
输出日志:
报告结果:
找到这个目录
打开这个html
好,整个框架完成了,只剩扩展一些边边角角的了
比如:下面打印日志,与发送邮件的,便不细说了,可以直接看代码学习学习,很简单的。
打印Log
相关代码
import loggingimport osfrom logging.handlers import TimedRotatingFileHandlerfrom common.getconf import Configfrom common.getfiledir import LOGDIRclass Handlogging(): @staticmethod def emplorlog(): conf = Config() # set format formatter = logging.Formatter("%(asctime)s - %(name)s-%(levelname)s %(message)s") # create log set getlog level mylog = logging.getLogger('HunWei') mylog.setLevel(conf.get('LOG', 'level')) # create outputsteam set level sh = logging.StreamHandler() sh.setLevel(conf.get('LOG', 'level')) sh.setFormatter(formatter) mylog.addHandler(sh) #create file set level #fh = logging.FileHandler(os.path.join(LOGDIR,'test.log'), encoding='utf-8') log_path = os.path.join(LOGDIR, 'test') # interval 滚动周期, # when="MIDNIGHT", interval=1 表示每天0点为更新点,每天生成一个文件 # backupCount 表示日志保存个数 fh = TimedRotatingFileHandler( filename=log_path, when="D", backupCount=15, encoding='utf-8' ) fh.suffix = "%Y-%m-%d.log" fh.setLevel(conf.get('LOG', 'level')) fh.setFormatter(formatter) mylog.addHandler(fh) return mylogmylog = Handlogging.emplorlog()
发送邮件
相关代码
import smtplibimport osfrom common.getconf import Configfrom email.mime.text import MIMETextfrom email.mime.multipart import MIMEMultipartfrom email.mime.application import MIMEApplicationfrom common.getfiledir import REPORTDIRclass Opr_email(object): def __init__(self): """ 初始化文件路径与相关配置 """ self.conf = Config() all_path = [] for maindir, subdir, file_list in os.walk(REPORTDIR): pass for filename in file_list: all_path.append(os.path.join(REPORTDIR, filename)) self.filename = all_path[0] self.host = self.conf.get('email', 'host') self.port = self.conf.get('email', 'port') self.user = self.conf.get('email', 'user') self.pwd = self.conf.get('email', 'pwd') self.from_addr = self.conf.get('email', 'from_addr') self.to_addr = self.conf.get('email', 'to_addr') def get_email_host_smtp(self): """ 连接stmp服务器 :return: """ self.smtp = smtplib.SMTP_SSL(host=self.host, port=self.port) self.smtp.login(user=self.user, password=self.pwd) def made_msg(self): """ 构建一封邮件 :return: """ self.msg = MIMEMultipart() with open(self.filename, 'rb') as f: content = f.read() # 创建文本内容 text_msg = MIMEText(content, _subtype='html', _charset='utf8') # 添加到多组件的邮件中 self.msg.attach(text_msg) # 创建邮件的附件 report_file = MIMEApplication(content) report_file.add_header('Content-Disposition', 'attachment', filename=str.split(self.filename, '\\').pop()) self.msg.attach(report_file) # 主题 self.msg['subject'] = '自动化测试报告' # 发件人 self.msg['From'] = self.from_addr # 收件人 self.msg['To'] = self.to_addr def send_email(self): """ 发送邮件 :return: """ self.get_email_host_smtp() self.made_msg() self.smtp.send_message(self.msg, from_addr=self.from_addr, to_addrs=self.to_addr)