小言_互联网的博客

Python 从无到有搭建WebUI自动化测试框架

277人阅读  评论(0)

目录

前言

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里写一段代码,


  
  1. from selenium import webdriver
  2. driver = webdriver.Chrome()
  3. driver.get('https://www.baidu.com')

run,结果是自动打开谷歌进入百度了,安排

 

3、封装浏览器--初始化浏览器

谷歌驱动能用,万事俱备只欠东风,现我们来实现这个东风

代码引用里有from common.getconf import Config要先实现一个Config类,因为我们要的基础类里要用到一个配置项,这也是为了以后做大做强的需要一个东西,先说一下,具体见【读取config配置

在config 目录下有一个配置文件,配置文件里配置了浏览器类型,只是为了以后能兼容三种浏览器,目前配置了chrome

首先我们新建名为BrowserOperator的类,初始化配置类的对象,获取到浏览器类型的配置项,得到驱动的目录。初始化用到了os

所以我们要import os


  
  1. class BrowserOperator(object):
  2. def __init__(self):
  3. self.conf = Config()
  4. self.driver_path = os.path.join(BASEFACTORYDIR, 'chromedriver.exe')
  5. self.driver_type = str(self.conf.get('base', 'browser_type')).lower()

 

然后我们在基础类里实现一个方法def open_url(self, **kwargs): 打开浏览器,方法使用了不定长参数 **kwargs接收传参,所以只能传指定参数或整个字典,


  
  1. def open_url(self, **kwargs):
  2. """
  3. 打开网页
  4. :param url:
  5. :return: 返回 webdriver
  6. """
  7. try:
  8. url = kwargs['locator']
  9. except KeyError:
  10. return False, '没有URL参数'

这一段,kwargs里如果有locator,这是用例用例设计的一个参数,它负责传url与元素定位,后面讲excel读取时会说到的。

可以写成url = kwargs.get('locator'),就不需要try了,但要判断url是不是为None,是None还是要return False, ‘没有参数’,两个返回值是全框架设计的返回形式,用一个布尔值确认我的用例执行的成败,后面返回对象或执行日志。至于我写try,个人代码风格。


  
  1. try:
  2. if self.driver_type == 'chrome':
  3. #处理chrom弹出的info
  4. # chrome_options = webdriver.ChromeOptions()
  5. # #option.add_argument('disable-infobars')
  6. # chrome_options.add_experimental_option("excludeSwitches", ['enable-automation'])
  7. # self.driver = webdriver.Chrome(options=chrome_options, executable_path=self.driver_path)
  8. self.driver = webdriver.Chrome(executable_path=self.driver_path)
  9. self.driver.maximize_window()
  10. self.driver.get(url)
  11. elif self.driver_type == 'ie':
  12. print('IE 浏览器')
  13. else:
  14. print('火狐浏览器')
  15. except Exception as e:
  16. return False, e
  17. 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文件下面写下两代码,然后运行


  
  1. wd = BrowserOperator()
  2. wd.open_url(locator='https://www.qq.com')

对的,locator='https://ww.....'    这就是指定传参

run,结果是打开了qq网站,说明代码已经执行成功了

 

然后我们改一下代码,


  
  1. wd = BrowserOperator()
  2. isOK, deiver = wd.open_url(locator='https://www.qq.com')
  3. time.sleep(5)
  4. deiver.find_elements_by_xpath('//*[@id="sougouTxt"]')[0].send_keys('飞人')
  5. deiver.find_elements_by_xpath('//*[@id="searchBtn"]')[0].click()

结果他开始搜索飞人了,说明第一步打开浏览器返回操作对象很成功。

 

4、关闭浏览器

在类下面实现一个方法def close_browser(self, **kwargs):


  
  1. def close_browser(self, **kwargs):
  2. """
  3. 关闭浏览器
  4. :return:
  5. """
  6. time.sleep(1)
  7. self.driver.quit()
  8. time.sleep(2)
  9. return True, '关闭浏览器成功'

里面的的睡眠时间很重要的,不然调试时肉眼看不到最后执行结果与否,浏览器就关掉了。

关键代码self.driver.quit(),这个方法退出浏览器,哈哈哈


  
  1. wd = BrowserOperator()
  2. isOK, deiver = wd.open_url(locator='https://www.qq.com')
  3. time.sleep(5)
  4. deiver.find_elements_by_xpath('//*[@id="sougouTxt"]')[0].send_keys('飞人')
  5. deiver.find_elements_by_xpath('//*[@id="searchBtn"]')[0].click()
  6. wd.close_browser()

在执行代码后面添加一行wd.close_browser()

run,看着他打开浏览器,搜索飞人,然后关闭浏览器,执行通过。

上传附件,后面写完用例模块时,才能涉及到。网上也有很多上传附件的实现方法。所以就不说了,里面涉及windows的消息机制,很高深,照搬代码即可。

browseroperator.py里所有的代码提供给各位看官,希望多给两个赞,谢谢


  
  1. import os
  2. import win32gui
  3. import win32con
  4. import time
  5. from selenium import webdriver
  6. from common.getconf import Config
  7. from common.getfiledir import BASEFACTORYDIR
  8. from pywinauto import application
  9. class BrowserOperator(object):
  10. def __init__(self):
  11. self.conf = Config()
  12. self.driver_path = os.path.join(BASEFACTORYDIR, 'chromedriver.exe')
  13. self.deriver_type = str(self.conf.get('base', 'browser_type')).lower()
  14. def open_url(self, **kwargs):
  15. """
  16. 打开网页
  17. :param url:
  18. :return: 返回 webdriver
  19. """
  20. try:
  21. url = kwargs['locator']
  22. except KeyError:
  23. return False, '没有URL参数'
  24. try:
  25. if self.deriver_type == 'chrome':
  26. #处理chrom弹出的info
  27. # chrome_options = webdriver.ChromeOptions()
  28. # #option.add_argument('disable-infobars')
  29. # chrome_options.add_experimental_option("excludeSwitches", ['enable-automation'])
  30. # self.driver = webdriver.Chrome(options=chrome_options, executable_path=self.driver_path)
  31. self.driver = webdriver.Chrome(executable_path=self.driver_path)
  32. self.driver.maximize_window()
  33. self.driver.get(url)
  34. elif self.deriver_type == 'ie':
  35. print('IE 浏览器')
  36. else:
  37. print('火狐浏览器')
  38. except Exception as e:
  39. return False, e
  40. return True, self.driver
  41. def close_browser(self, **kwargs):
  42. """
  43. 关闭浏览器
  44. :return:
  45. """
  46. time.sleep(1)
  47. self.driver.quit()
  48. time.sleep(2)
  49. return True, '关闭浏览器成功'
  50. def upload_file(self, **kwargs):
  51. """
  52. 上传文件
  53. :param kwargs:
  54. :return:
  55. """
  56. try:
  57. dialog_class = kwargs['type']
  58. file_dir = kwargs['locator']
  59. button_name = kwargs['index']
  60. except KeyError:
  61. return True, '没传对话框的标记或没传文件路径,'
  62. if self.deriver_type == "chrome":
  63. title = "打开"
  64. elif self.deriver_type == "firefox":
  65. title = "文件上传"
  66. elif self.deriver_type == "ie":
  67. title = "选择要加载的文件"
  68. else:
  69. title = "" # 这里根据其它不同浏览器类型来修改
  70. # 找元素
  71. # 一级窗口"#32770","打开"
  72. dialog = win32gui.FindWindow(dialog_class, title)
  73. if dialog == 0:
  74. return False, '传入对话框的class定位器有误'
  75. # 向下传递
  76. ComboBoxEx32 = win32gui.FindWindowEx(dialog, 0, "ComboBoxEx32", None) # 二级
  77. comboBox = win32gui.FindWindowEx(ComboBoxEx32, 0, "ComboBox", None) # 三级
  78. # 编辑按钮
  79. edit = win32gui.FindWindowEx(comboBox, 0, 'Edit', None) # 四级
  80. # 打开按钮
  81. button = win32gui.FindWindowEx(dialog, 0, 'Button', button_name) # 二级
  82. if button == 0:
  83. return False, '按钮text属性传值有误'
  84. # 输入文件的绝对路径,点击“打开”按钮
  85. win32gui.SendMessage(edit, win32con.WM_SETTEXT, None, file_dir) # 发送文件路径
  86. time.sleep(1)
  87. win32gui.SendMessage(dialog, win32con.WM_COMMAND, 1, button) # 点击打开按钮
  88. return True, '上传文件成功'

页面操作

浏览器的代码先告一段落,接着得实现页面元素的操作类了

1、引用模块,导包


  
  1. import os
  2. import time
  3. from selenium.common.exceptions import NoSuchElementException, TimeoutException
  4. from selenium.webdriver import Chrome
  5. from selenium.webdriver.support import expected_conditions as EC
  6. from selenium.webdriver.support.ui import WebDriverWait
  7. from selenium.webdriver.common.by import By
  8. from 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对象,所以初始化这个对象为私有属性


  
  1. class WebdriverOperator(object):
  2. def __init__(self, driver:Chrome):
  3. 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文件中,最后返回路径,具休代码如下


  
  1. def get_screenshot_as_file(self):
  2. """
  3. 截屏保存
  4. :return:返回路径
  5. """
  6. pic_name = str.split(str(time.time()), '.')[0] + str.split(str(time.time()), '.')[1] + '.png'
  7. screent_path = os.path.join(SCREENSHOTDIR, pic_name)
  8. self.driver.get_screenshot_as_file(screent_path)
  9. return screent_path

我们来调试一下这个代码

在basefactory目录下面请建一个test.py文件


  
  1. from basefactory.browseroperator import BrowserOperator
  2. from basefactory.webdriveroperator import WebdriverOperator
  3. bo = BrowserOperator()
  4. isOK, deiver = bo.open_url(locator='https://www.qq.com')
  5. wb = WebdriverOperator(deiver)
  6. result = wb.get_screenshot_as_file() #调用截图方法,返回路径打印
  7. print(result)

输入这么一段代码。

run,它会打开浏览器,进入qq网站主页,然后打印截图文件目录

找到对应目录 与文件,截图方法调试成功

 

3、隐式等待

gotosleep(),这个方法不说了,死等。

隐式等待,等待页面元素加载成功便完成等待任务,只需要在用例初始化浏览器之处调用一次,全局可用,其实他只是一行代码,但为了符合框架统一,也封装一下,代码如下:


  
  1. def web_implicitly_wait(self, **kwargs):
  2. """
  3. 隐式等待
  4. :return:
  5. type 存时间
  6. """
  7. try:
  8. s = kwargs['time']
  9. except KeyError:
  10. s = 10
  11. try:
  12. self.driver.implicitly_wait(s)
  13. except NoSuchElementException:
  14. return False, '隐式等待设置失败'
  15. return True, '隐式等待设置成功'

其实只有一行代码 self.driver.implicitly_wait(s) 有用的,里面的s是用例传过来的,调试时,只需要传指定time传一个数字,例:time=5,每次页面刷新,程序将等待页面元素加载5秒,5秒后,不管加载成功与否都执行下一行代码,如果2秒有加载完,那么不必等5秒,直接执行下一行代码

切记,隐式等待只需要初始化浏览器调用一次,后面的代码都会隐式等待。

因为我本地网速太好,打开了两个网络视频播放器去调试网页都没法使网页长时间加载,所以只贴上调试代码,结果自己看吧。

在test.py里更新调试代码


  
  1. from basefactory.browseroperator import BrowserOperator
  2. from basefactory.webdriveroperator import WebdriverOperator
  3. bo = BrowserOperator()
  4. isOK, deiver = bo.open_url(locator='https://www.baidu.com')
  5. wb = WebdriverOperator(deiver)
  6. isOK, result = wb.web_implicitly_wait() #设置隐式等待,打印隐式等待的结果
  7. print(result)
  8. deiver.find_elements_by_xpath('//*[@id="kw"]')[0].send_keys('飞人')
  9. deiver.find_elements_by_xpath('//*[@id="su"]')[0].click()
  10. deiver.find_elements_by_xpath('//*[@id="rs"]/div')

4、显示等待

隐式等待在页面切换后,加载成功但没展示出来,disable,hide等场景时,也能等待成功。所以不能满足需求。我们需要更强大的等待元素的方法,显示等待,代码如下:


  
  1. def web_element_wait(self, **kwargs):
  2. """
  3. 等待元素可见
  4. :return:
  5. """
  6. try:
  7. type = kwargs['type']
  8. locator = kwargs['locator']
  9. except KeyError:
  10. return False, '未传需要等待元素的定位参数'
  11. try:
  12. s = kwargs['time']
  13. if s is None:
  14. s = 30
  15. except KeyError:
  16. s = 30
  17. try:
  18. if type == 'id':
  19. WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.ID, locator)))
  20. elif type == 'name':
  21. WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.NAME, locator)))
  22. elif type == 'class':
  23. WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.CLASS_NAME, locator)))
  24. elif type == 'xpath':
  25. WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.XPATH, locator)))
  26. elif type == 'css':
  27. WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.CSS_SELECTOR, locator)))
  28. else:
  29. return False, '不能识别元素类型[' + type + ']'
  30. except TimeoutException:
  31. screenshot_path = self.get_screenshot_as_file()
  32. return False, '元素[' + locator + ']等待出现失败,已截图[' + screenshot_path + '].'
  33. 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里修改代码如下


  
  1. from basefactory.browseroperator import BrowserOperator
  2. from basefactory.webdriveroperator import WebdriverOperator
  3. bo = BrowserOperator()
  4. isOK, deiver = bo.open_url(locator='https://www.baidu.com')
  5. wb = WebdriverOperator(deiver)
  6. isOK, result = wb.web_element_wait(type='xpath', locator='//*[@id="kw"]', s=0.001)
  7. print(result)

设置0.001秒,也能成功,网速太快了,失败的例子等各位来重现了,不想去使用高延迟工具了。

 

5、输入操作

输入,元素的send_key() 方法,所以在WebdriverOperator类里实现一个element_input方法,代码如下:


  
  1. def element_input(self, **kwargs):
  2. """
  3. :param kwargs:
  4. :return:
  5. """
  6. try:
  7. type = kwargs['type']
  8. locator = kwargs['locator']
  9. text = str(kwargs['input'])
  10. except KeyError:
  11. return False, '缺少传参'
  12. try:
  13. index = kwargs['index']
  14. except KeyError:
  15. index = 0
  16. try:
  17. if type == 'id':
  18. elem = self.driver.find_elements_by_id(locator)[index]
  19. elif type == 'name':
  20. elem = self.driver.find_elements_by_name(locator)[index]
  21. elif type == 'class':
  22. elem = self.driver.find_elements_by_class_name(locator)[index]
  23. elif type == 'xpath':
  24. elem = self.driver.find_elements_by_xpath(locator)[index]
  25. elif type == 'css':
  26. elem = self.driver.find_elements_by_css_selector(locator)[index]
  27. else:
  28. return False, '不能识别元素类型:[' + type + ']'
  29. except Exception as e:
  30. screenshot_path = self.get_screenshot_as_file()
  31. return False, '获取[' + type + ']元素[' + locator + ']失败,已截图[' + screenshot_path + '].'
  32. try:
  33. elem.send_keys(text)
  34. except Exception:
  35. screenshot_path = self.get_screenshot_as_file()
  36. return False, '元素['+ locator +']输入['+ text +']失败,已截图[' + screenshot_path + '].'
  37. return True, '元素['+ locator +']输入['+ text +']成功'

 

代码


  
  1. type = kwargs['type']
  2. locator = kwargs['locator']
  3. text = str(kwargs['input'])
  4. 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

这一部分代码


  
  1. try:
  2. if type == 'id':
  3. elem = self.driver.find_elements_by_id(locator)[index]
  4. elif type == 'name':
  5. elem = self.driver.find_elements_by_name(locator)[index]
  6. elif type == 'class':
  7. elem = self.driver.find_elements_by_class_name(locator)[index]
  8. elif type == 'xpath':
  9. elem = self.driver.find_elements_by_xpath(locator)[index]
  10. elif type == 'css':
  11. elem = self.driver.find_elements_by_css_selector(locator)[index]
  12. else:
  13. return False, '不能识别元素类型:[' + type + ']'
  14. except Exception as e:
  15. screenshot_path = self.get_screenshot_as_file()
  16. return False, '获取[' + type + ']元素[' + locator + ']失败,已截图[' + screenshot_path + '].'

是用来查找页面元素是否存在的,如果存在,将元素找到存在elem变量,继续执行,如果失败,截图,返回False与失败日志

通过type判断,我们使用哪种元素定位方法,这些是基础,不懂的朋友可百度一下寻找元素定位的方法。

输入input的代码:


  
  1. try:
  2. elem.send_keys(text)
  3. except Exception:
  4. screenshot_path = self.get_screenshot_as_file()
  5. return False, '元素['+ locator +']输入['+ text +']失败,已截图[' + screenshot_path + '].'
  6. return True, '元素['+ locator +']输入['+ text +']成功'

输入成功,返回True与成功日志,失败,截图并返回False与失败日志

然后我们来调试一把,在test.py文件里更新代码如下:


  
  1. bo = BrowserOperator()
  2. isOK, deiver = bo.open_url(locator='https://www.baidu.com')
  3. wb = WebdriverOperator(deiver)
  4. isOK, result = wb.web_element_wait(type='xpath', locator='//*[@id="kw"]', s=0.001)
  5. print(result)
  6. isOK, result = wb.element_input(type='xpath', locator='//*[@id="kw"]', input='飞人', index=0) #元素输入方法
  7. print(result)

run,结果打开百度,在编搜索框输入 ‘飞人’,因为还没做点击,先不click百度一下了

再看打印的运行日志

运行成功。

 

6、优化输入方法

输入方法中的find页面元素是否存在的代码,很多元素操作都需要的。所以再把它分离出来写一个单独的方法find_element,代码如下:


  
  1. def find_element(self, type, locator, index=None):
  2. """
  3. 定位元素
  4. :param type:
  5. :param itor:
  6. :param index:
  7. :return:
  8. """
  9. time.sleep(1)
  10. #isinstance(self.driver, selenium.webdriver.Chrome.)
  11. if index is None:
  12. index = 0
  13. type = str.lower(type)
  14. try:
  15. if type == 'id':
  16. elem = self.driver.find_elements_by_id(locator)[index]
  17. elif type == 'name':
  18. elem = self.driver.find_elements_by_name(locator)[index]
  19. elif type == 'class':
  20. elem = self.driver.find_elements_by_class_name(locator)[index]
  21. elif type == 'xpath':
  22. elem = self.driver.find_elements_by_xpath(locator)[index]
  23. elif type == 'css':
  24. elem = self.driver.find_elements_by_css_selector(locator)[index]
  25. else:
  26. return False, '不能识别元素类型:[' + type + ']'
  27. except Exception as e:
  28. screenshot_path = self.get_screenshot_as_file()
  29. return False, '获取[' + type + ']元素[' + locator + ']失败,已截图[' + screenshot_path + '].'
  30. return True, elem

然后我们的输入方法代码修改为:


  
  1. def element_input(self, **kwargs):
  2. """
  3. :param kwargs:
  4. :return:
  5. """
  6. try:
  7. type = kwargs['type']
  8. locator = kwargs['locator']
  9. text = str(kwargs['input'])
  10. except KeyError:
  11. return False, '缺少传参'
  12. try:
  13. index = kwargs['index']
  14. except KeyError:
  15. index = 0
  16. isOK, result = self.find_element(type, locator, index)
  17. if not isOK: # 元素没找到,返回失败结果
  18. return isOK, result
  19. elem = result
  20. try:
  21. elem.send_keys(text)
  22. except Exception:
  23. screenshot_path = self.get_screenshot_as_file()
  24. return False, '元素['+ locator +']输入['+ text +']失败,已截图[' + screenshot_path + '].'
  25. return True, '元素['+ locator +']输入['+ text +']成功'

继续使用用test.py中的代码调试,运行结果与第5点输入操作的运行结果一样,就优化成功了。

 

7、点击操作

点击操作,元素的click()方法,没有什么好说的,因为他的代码与第5点输入操作一样,只是少了个input参数。所以直接贴代码


  
  1. def element_click(self, **kwargs):
  2. """
  3. :param kwargs:
  4. :return:
  5. """
  6. try:
  7. type = kwargs['type']
  8. locator = kwargs['locator']
  9. except KeyError:
  10. return False, '缺少传参'
  11. try:
  12. index = kwargs['index']
  13. except KeyError:
  14. index = 0
  15. isOK, result = self.find_element(type, locator, index)
  16. if not isOK: #元素没找到,返回失败结果
  17. return isOK, result
  18. elem = result
  19. try:
  20. elem.click()
  21. except Exception:
  22. screenshot_path = self.get_screenshot_as_file()
  23. return False, '元素['+ locator +']点击失败,已截图[' + screenshot_path + '].'
  24. return True, '元素['+ locator +']点击成功'

 

调试,在test.py里更新代码


  
  1. bo = BrowserOperator()
  2. isOK, deiver = bo.open_url(locator='https://www.baidu.com')
  3. wb = WebdriverOperator(deiver)
  4. isOK, result = wb.web_element_wait(type='xpath', locator='//*[@id="kw"]', s=0.001)
  5. print(result)
  6. isOK, result = wb.element_input(type='xpath', locator='//*[@id="kw"]', input='飞人', index=0)
  7. print(result)
  8. isOK, result = wb.element_click(type='xpath', locator='//*[@id="su"]', index=0) #元素点击方法
  9. print(result)

run,运行结果,一切正常

 

好,基本元素操作的代码解说完了,全部代码如下,不想自己写的可以copy。

还缺少执行JS与一些偏门控件的方法,这些给各位去自个儿写。


  
  1. import os
  2. import time
  3. from selenium.common.exceptions import NoSuchElementException, TimeoutException
  4. from selenium.webdriver import Chrome
  5. from selenium.webdriver.support import expected_conditions as EC
  6. from selenium.webdriver.support.ui import WebDriverWait
  7. from selenium.webdriver.common.by import By
  8. from common.getfiledir import SCREENSHOTDIR
  9. class WebdriverOperator(object):
  10. def __init__(self, driver:Chrome):
  11. self.driver = driver
  12. def get_screenshot_as_file(self):
  13. """
  14. 截屏保存
  15. :return:返回路径
  16. """
  17. pic_name = str.split(str(time.time()), '.')[0] + str.split(str(time.time()), '.')[1] + '.png'
  18. screent_path = os.path.join(SCREENSHOTDIR, pic_name)
  19. self.driver.get_screenshot_as_file(screent_path)
  20. return screent_path
  21. def gotosleep(self, **kwargs):
  22. time.sleep(3)
  23. return True, '等待成功'
  24. def web_implicitly_wait(self, **kwargs):
  25. """
  26. 隐式等待
  27. :return:
  28. type 存时间
  29. """
  30. try:
  31. s = kwargs['time']
  32. except KeyError:
  33. s = 10
  34. try:
  35. self.driver.implicitly_wait(s)
  36. except NoSuchElementException:
  37. return False, '隐式等待 页面元素未加载完成'
  38. return True, '隐式等待 元素加载完成'
  39. def web_element_wait(self, **kwargs):
  40. """
  41. 等待元素可见
  42. :return:
  43. """
  44. try:
  45. type = kwargs['type']
  46. locator = kwargs['locator']
  47. except KeyError:
  48. return False, '未传需要等待元素的定位参数'
  49. try:
  50. s = kwargs['time']
  51. if s is None:
  52. s = 30
  53. except KeyError:
  54. s = 30
  55. try:
  56. if type == 'id':
  57. WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.ID, locator)))
  58. elif type == 'name':
  59. WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.NAME, locator)))
  60. elif type == 'class':
  61. WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.CLASS_NAME, locator)))
  62. elif type == 'xpath':
  63. WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.XPATH, locator)))
  64. elif type == 'css':
  65. WebDriverWait(self.driver, s, 0.5).until(EC.visibility_of_element_located((By.CSS_SELECTOR, locator)))
  66. else:
  67. return False, '不能识别元素类型[' + type + ']'
  68. except TimeoutException:
  69. screenshot_path = self.get_screenshot_as_file()
  70. return False, '元素[' + locator + ']等待出现失败,已截图[' + screenshot_path + '].'
  71. return True, '元素[' + locator + ']等待出现成功'
  72. def find_element(self, type, locator, index=None):
  73. """
  74. 定位元素
  75. :param type:
  76. :param itor:
  77. :param index:
  78. :return:
  79. """
  80. time.sleep(1)
  81. #isinstance(self.driver, selenium.webdriver.Chrome.)
  82. if index is None:
  83. index = 0
  84. type = str.lower(type)
  85. try:
  86. if type == 'id':
  87. elem = self.driver.find_elements_by_id(locator)[index]
  88. elif type == 'name':
  89. elem = self.driver.find_elements_by_name(locator)[index]
  90. elif type == 'class':
  91. elem = self.driver.find_elements_by_class_name(locator)[index]
  92. elif type == 'xpath':
  93. elem = self.driver.find_elements_by_xpath(locator)[index]
  94. elif type == 'css':
  95. elem = self.driver.find_elements_by_css_selector(locator)[index]
  96. else:
  97. return False, '不能识别元素类型:[' + type + ']'
  98. except Exception as e:
  99. screenshot_path = self.get_screenshot_as_file()
  100. return False, '获取[' + type + ']元素[' + locator + ']失败,已截图[' + screenshot_path + '].'
  101. return True, elem
  102. def element_click(self, **kwargs):
  103. """
  104. :param kwargs:
  105. :return:
  106. """
  107. try:
  108. type = kwargs['type']
  109. locator = kwargs['locator']
  110. except KeyError:
  111. return False, '缺少传参'
  112. try:
  113. index = kwargs['index']
  114. except KeyError:
  115. index = 0
  116. isOK, result = self.find_element(type, locator, index)
  117. if not isOK: #元素没找到,返回失败结果
  118. return isOK, result
  119. elem = result
  120. try:
  121. elem.click()
  122. except Exception:
  123. screenshot_path = self.get_screenshot_as_file()
  124. return False, '元素['+ locator +']点击失败,已截图[' + screenshot_path + '].'
  125. return True, '元素['+ locator +']点击成功'
  126. def element_input(self, **kwargs):
  127. """
  128. :param kwargs:
  129. :return:
  130. """
  131. try:
  132. type = kwargs['type']
  133. locator = kwargs['locator']
  134. text = str(kwargs['input'])
  135. except KeyError:
  136. return False, '缺少传参'
  137. try:
  138. index = kwargs['index']
  139. except KeyError:
  140. index = 0
  141. isOK, result = self.find_element(type, locator, index)
  142. if not isOK: # 元素没找到,返回失败结果
  143. return isOK, result
  144. elem = result
  145. try:
  146. elem.send_keys(text)
  147. except Exception:
  148. screenshot_path = self.get_screenshot_as_file()
  149. return False, '元素['+ locator +']输入['+ text +']失败,已截图[' + screenshot_path + '].'
  150. return True, '元素['+ locator +']输入['+ text +']成功'

 

4、公共类

获取框架项目目录的绝对路径

先在common目录下新增一个,getfiledir.py的文件

这一段没什么多说的,就是获取本框架各目录的绝对路径


  
  1. import os
  2. dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  3. DATADIR = os.path.join(dir, 'data')
  4. CONFDIR = os.path.join(dir, 'config')
  5. BASEFACTORYDIR = os.path.join(dir, 'basefactory')
  6. RESULTDIR = os.path.join(dir, 'result')
  7. LOGDIR = os.path.join(RESULTDIR, 'log')
  8. REPORTDIR = os.path.join(RESULTDIR, 'report')
  9. SCREENSHOTDIR = os.path.join(RESULTDIR, 'screenshot')
  10. CASEDIR = os.path.join(dir, 'excutetest')
  11. #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


  
  1. class ReadCase(object):
  2. def __init__(self):
  3. self.sw = openpyxl.load_workbook(file)
  4. print(self.sw)

初始化类,顺便读取一下用例文件

调试

在文件里输入,

xlsx = ReadCase()

run,打印出这么一串人类不认识,那么就成功了,他打印了一个内存对象,保存了整个case文件

 

4、读取单个sheet页用例

实现一个方法readcase, 代码如下


  
  1. def readcase(self, sh):
  2. """
  3. 组合sheet页的数据
  4. :param sh:
  5. :return: list,返回组合数据
  6. """
  7. if sh is None:
  8. return False, '用例页参数未传'
  9. datas = list(sh.rows)
  10. if datas == []:
  11. return False, '用例[' + sh.title + ']里面是空的'
  12. title = [i.value for i in datas[0]]
  13. rows = []
  14. sh_dict = {}
  15. for i in datas[1:]:
  16. data = [v.value for v in i]
  17. row = dict(zip(title, data))
  18. try:
  19. if str(row['id'])[0] is not '#':
  20. row['sheet'] = sh.title
  21. rows.append(row)
  22. except KeyError:
  23. raise e
  24. rows.append(row)
  25. sh_dict[sh.title] = rows
  26. return True, sh_dict

参数传入一个sheet页对象,

然后判空,


  
  1. if sh is None:
  2. return False, '用例页参数未传'

通过列表保存sheet的每一行,判空


  
  1. datas = list(sh.rows)
  2. if datas == []:
  3. return False, '用例[' + sh.title + ']里面是空的'

得到第一行为title,为啥呢,看excel文件的格式

title = [i.value for i in datas[0]]

用一个循环得到每一行的用例与title结合成一个字典json串,返回


  
  1. for i in datas[1:]:
  2. data = [v.value for v in i]
  3. row = dict(zip(title, data))
  4. try:
  5. if str(row['id'])[0] is not '#':
  6. row['sheet'] = sh.title
  7. rows.append(row)
  8. except KeyError:
  9. raise e
  10. rows.append(row)
  11. sh_dict[sh.title] = rows
  12. return True, sh_dict

好了,来调试一发,在下面输入代码:


  
  1. xlsx = ReadCase()
  2. for sh in xlsx.sw:
  3. isOK, result = xlsx.readcase(sh)
  4. 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读取所有用例,代码如下:


  
  1. def readallcase(self):
  2. """
  3. 取所有sheet页
  4. :return:list,返回sheet页里的数据
  5. """
  6. sheet_list = []
  7. for sh in self.sw: #遍历sheet,
  8. if 'common' != sh.title.split('_')[0] and 'common' != sh.title.split('-')[0] and sh.title[0] is not '#' : #判断是否可用的用例
  9. isOK, result = self.readcase(sh) #传给readcase取用例
  10. if isOK:
  11. sheet_list.append(result) #得到结果放到列表,又给用例套了一层sheet页的框
  12. if sheet_list is None:
  13. return False, '用例集是空的,请检查用例'
  14. return True, sheet_list

这里面有一个判断条件,是公共用例,common开头的用例不读取,注释用例以#号开头的用例,不读取

调试代码


  
  1. xlsx = ReadCase()
  2. isOK, result = xlsx.readallcase()
  3. print(result)

运行结果,请看比第4点运行的结果多了一个中括号,表示他已经可以读取所有的用例了

 

6、读取公共用例

实现一个get_common_case方法,方法很简单,查询是否有传参sheetname的sheet页存在,有的话就读取sheet页的用例,返回,代码如下。


  
  1. def get_common_case(self, case_name):
  2. """
  3. 得到公共用例
  4. :param case_name:
  5. :return:
  6. """
  7. try:
  8. sh = self.sw.get_sheet_by_name(case_name)
  9. except KeyError:
  10. return False, '未找到公共用例[' + case_name + '],请检查用例'
  11. except DeprecationWarning:
  12. pass
  13. return self.readcase(sh)

要结合核心模块的调用

调试代码如下


  
  1. xlsx = ReadCase()
  2. isOK, result = xlsx.get_common_case('baidu')
  3. print(result)

结果如第5点结果一样

 

读取用例的全部代码如下:


  
  1. import openpyxl
  2. import os
  3. from common.getfiledir import DATADIR
  4. file = os.path.join(DATADIR, 'case.xlsx')
  5. class ReadCase(object):
  6. def __init__(self):
  7. self.sw = openpyxl.load_workbook(file)
  8. print(self.sw)
  9. def openxlsx(self, file):
  10. """
  11. 打开文件
  12. :param dir:
  13. :return:
  14. """
  15. # self.sw = openpyxl.load_workbook(file)
  16. #[{"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]}]
  17. def readallcase(self):
  18. """
  19. 取所有sheet页
  20. :return:list,返回sheet页里的数据
  21. """
  22. sheet_list = []
  23. for sh in self.sw:
  24. if 'common' != sh.title.split('_')[0] and 'common' != sh.title.split('-')[0] and sh.title[0] is not '#' :
  25. isOK, result = self.readcase(sh)
  26. if isOK:
  27. sheet_list.append(result)
  28. if sheet_list is None:
  29. return False, '用例集是空的,请检查用例'
  30. return True, sheet_list
  31. def readcase(self, sh):
  32. """
  33. 组合sheet页的数据
  34. :param sh:
  35. :return: list,返回组合数据
  36. """
  37. if sh is None:
  38. print('sheet页为空')
  39. datas = list(sh.rows)
  40. if datas == []:
  41. return False, '用例[' + sh.title + ']里面是空的'
  42. title = [i.value for i in datas[0]]
  43. rows = []
  44. sh_dict = {}
  45. for i in datas[1:]:
  46. data = [v.value for v in i]
  47. row = dict(zip(title, data))
  48. try:
  49. if str(row['id'])[0] is not '#':
  50. row['sheet'] = sh.title
  51. rows.append(row)
  52. except KeyError:
  53. raise e
  54. rows.append(row)
  55. sh_dict[sh.title] = rows
  56. return True, sh_dict
  57. def get_common_case(self, case_name):
  58. """
  59. 得到公共用例
  60. :param case_name:
  61. :return:
  62. """
  63. try:
  64. sh = self.sw.get_sheet_by_name(case_name)
  65. except KeyError:
  66. return False, '未找到公共用例[' + case_name + '],请检查用例'
  67. except DeprecationWarning:
  68. pass
  69. return self.readcase(sh)

读取config配置

目录:

Config类没有什么好解析的,暂时是为了封装而封装

具体代码如下:


  
  1. import os
  2. from configparser import ConfigParser
  3. from common.getfiledir import CONFDIR
  4. class Config(ConfigParser):
  5. def __init__(self):
  6. self.conf_name = os.path.join(CONFDIR, 'base.ini')
  7. super().__init__()
  8. super().read(self.conf_name, encoding='utf-8')
  9. def save_data(self, section, option, value):
  10. super().set(section=section, option=option, value=value)
  11. super().write(fp=open(self.conf_name, 'w'))

配置文件分析一下

目录

内容


  
  1. [base]
  2. browser_type = chrome
  3. # browser_type = IE
  4. # browser_type = firefox
  5. [LOG]
  6. level = INFO

  
  1. [email]
  2. host = smtp.qq.com
  3. port = 465
  4. user = xxxxx@qq.com
  5. pwd = *********
  6. from_addr = xxxxx@qq.com
  7. to_addr = xxxxx@qq.com

  
  1. [Function]
  2. 打开网页 = open_url
  3. 关闭浏览器 = close_browser
  4. 对话框上传文件 = upload_file
  5. 点击 = element_click
  6. 输入 = element_input
  7. 截图 = get_screenshot_as_file
  8. 寻找元素 = find_element
  9. 隐式等待 = web_implicitly_wait
  10. 等待元素可见 = web_element_wait
  11. 等待 = gotosleep
  12. input上传文件 = upload_input_file
  13. 模拟按键上传文件 = upload_endkey_file
  14. 切换页面 = toggle_page
  15. 执行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、引用模块,导包


  
  1. from common.getconf import Config
  2. from common.getcase import ReadCase
  3. from basefactory.browseroperator import BrowserOperator
  4. from basefactory.webdriveroperator import WebdriverOperator

导入了Config,配置文件读取

导入了ReadCase,来初始化用例

导入了BrowserOperator,WebdriverOperator,初始化这个两个类,执行用例

 

2、初始化工厂类


  
  1. class Factory(object):
  2. def __init__(self):
  3. self.con = Config()
  4. self.con_fun = dict(self.con.items('Function'))
  5. """
  6. 浏览器操作对象
  7. """
  8. self.browser_opr = BrowserOperator()
  9. """
  10. 网页操作对象
  11. """
  12. self.webdriver_opr = None

 

将配置项Function初始化进来


  
  1. self.con = Config()
  2. self.con_fun = dict(self.con.items('Function'))

再初始化一下两个基础类对象,因为WebdriverOperator类要在打开URL后才能初始化,先初始化为None

self.browser_opr = BrowserOperator()

self.webdriver_opr = None

然后我们将初始化webdriver_opr的代码放在一个函数里面,方便后面执行方法里面调用,里面强制转换一下,不然很难联想其对应的方法


  
  1. def init_webdriver_opr(self, driver):
  2. self.webdriver_opr = WebdriverOperator(driver)

下面我们先说如何实现代替PO模型的用例初始化

3、初始化执行用例

初始化执行用例 def init_execute_case(self): 里面的内容:


  
  1. def init_execute_case(self):
  2. print("----------初始化用例----------")
  3. xlsx = ReadCase()
  4. isOK, result = xlsx.readallcase()
  5. if not isOK:
  6. print(result)
  7. print("----------结束执行----------")
  8. exit()
  9. all_cases = result
  10. excu_cases = []
  11. for cases_dict in all_cases:
  12. for key, cases in cases_dict.items():
  13. isOK, result = self.init_common_case(cases)
  14. if isOK:
  15. cases_dict[key] = result
  16. else:
  17. cases_dict[key] = cases
  18. excu_cases.append(cases_dict)
  19. print("----------初始化用例完成----------")
  20. return excu_cases

一、读取了excel里所有可执行用例

二、调用用初始化公共用例

4、初始化公共用例

#初始化公共用例 def init_common_case(self, cases): 里面的内容:


  
  1. def init_common_case(self, cases):
  2. """
  3. :param kwargs:
  4. :return:
  5. """
  6. cases_len = len(cases)
  7. index = 0
  8. for case in cases:
  9. if case['keyword'] == '调用用例':
  10. xlsx = ReadCase()
  11. try:
  12. case_name = case['locator']
  13. except KeyError:
  14. return False, '调用用例没提供用例名,请检查用例'
  15. isOK, result = xlsx.get_common_case(case_name)
  16. if isOK and type([]) == type(result):
  17. isOK, result_1 = self.init_common_case(result) #递归检查公共用例里是否存在调用用例
  18. elif not isOK:
  19. return isOK, result
  20. list_rows = result[case_name]
  21. cases[index: index+1] = list_rows #将公共用例插入到执行用例中去
  22. index += 1
  23. if cases_len == index:
  24. return False, ''
  25. return True, cases

一、判断是否有‘调用用例’命令,有则取公共用例合并成可执行用例

二、递归取公共用例里是否有‘调用用例’命令,有则继续取公共合并成可执行用例

注意:公共用例不能调用自已,递归死循环

调试这两个方法一起

首先人们得在用例里面写上一个公共用例

再在baidu用例里写一行,调用用例

然后你们看看,这是不是就是PO模型,只需要在excel里面写写就行,完全不需要代码了

我们来调试一下,大工厂类里写下调试代码


  
  1. fac = Factory()
  2. isOK, result = fac.init_execute_case()
  3. print(result)

运行结果如下,我们初始化用例后,会把调用用例里被调用例的用例插入到执行用例中来,全部去执行。

 

下面来讲执行部分的

 

5、获取执行方法

获取两个基础WebdriverOperator、BrowserOperator的方法,具体代码如下:


  
  1. def get_base_function(self, function_name):
  2. try:
  3. function = getattr(self.browser_opr, function_name)
  4. except Exception:
  5. try:
  6. function = getattr(self.webdriver_opr, function_name)
  7. except Exception:
  8. return False, '未找到注册方法[' + function_name + ']所对应的执行函数,请检查配置文件'
  9. return True, function

传入方法名称function_name,通过getattr得到基础类的方法,成功得到方法,返回True,function,没有得到,返回False,日志

 

6、方法执行

实现一个方法,execute_keyword,统一入口调用两个基础类的操作

先放代码,再讲原理,代码如下:


  
  1. def execute_keyword(self, **kwargs):
  2. """
  3. 工厂函数,用例执行方法的入口
  4. :param kwargs:
  5. :return:
  6. """
  7. try:
  8. keyword = kwargs['keyword']
  9. if keyword is None:
  10. return False, '没有keyword,请检查用例'
  11. except KeyError:
  12. return False, '没有keyword,请检查用例'
  13. _isbrowser = False
  14. try:
  15. function_name = self.con_fun[keyword]
  16. except KeyError:
  17. return False, '方法Key['+ keyword +']未注册,请检查用例'
  18. #获取基础类方法
  19. isOK, result = self.get_base_function(function_name)
  20. if isOK:
  21. function = result
  22. else:
  23. return isOK, result
  24. #执行基础方法,如打网点页、点击、定位、隐式等待 等
  25. isOK, result = function(**kwargs)
  26. #如果是打开网页,是浏览器初始化,需要将返回值传递给另一个基础类
  27. if '打开网页' == keyword and isOK:
  28. url = kwargs['locator']
  29. self.init_webdriver_opr(result)
  30. return isOK, '网页[' + url + ']打开成功'
  31. return isOK, result

原理:先得到用例里的keyword,然后获取到两个基础类里方法,再传入**kwargs调用,执行操作

取到keyword关键字,回顾上面用例excel里的有一个keyword的字段,传入进来,先取这个字段


  
  1. try:
  2. keyword = kwargs['keyword']
  3. if keyword is None:
  4. return False, '没有keyword,请检查用例'
  5. except KeyError:
  6. return False, '没有keyword,请检查用例'

在self.con_fun键值对里取到keyword对应的方法名,具体方法可见配置文件那一节


  
  1. try:
  2. function_name = self.con_fun[keyword]
  3. except KeyError:
  4. return False, '方法Key['+ keyword +']未注册,请检查用例'

通过get_base_function得到基础类的方法


  
  1. isOK, result = self.get_base_function(function_name)
  2. if isOK:
  3. function = result
  4. else:
  5. return isOK, result

 

执行方法,返回结果


  
  1. #执行基础方法,如打网点页、点击、定位、隐式等待 等
  2. isOK, result = function(**kwargs)

 

当然,里面有一个特别的,如果方法是打开浏览器,这时就要初始化一下self.webdriver


  
  1. if '打开网页' == keyword and isOK:
  2. url = kwargs['locator']
  3. self.init_webdriver_opr(result)
  4. return isOK, '网页[' + url + ']打开成功'
  5. 最后我们返回执行结果
  6. return isOK, result

 

调试,我们在common里面新增一个test.py文件,在里面输入调试代码

导入Factory,初始化一个工厂对象fac,用fac,调用execute_keyword()方法来执行所有网页操作


  
  1. from common.factory import Factory
  2. fac = Factory()
  3. isOK, result = fac.execute_keyword(keyword='打开网页', locator='http://www.baidu.com')
  4. print(result)
  5. isOK, result = fac.execute_keyword(keyword='隐式等待', time='30')
  6. print(result)
  7. isOK, result = fac.execute_keyword(keyword='输入', type='xpath', locator='//*[@id="kw"]',input='飞人乔丹')
  8. print(result)
  9. idOK, result = fac.execute_keyword(keyword='点击', type='xpath', locator='//*[@id="su"]')
  10. 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)


  
  1. from common.factory import Factory
  2. fac = Factory()
  3. isOK, result = fac.init_execute_case()
  4. for acases in result:
  5. for key, cases in acases.items():
  6. for case in cases:
  7. isOK, result = fac.execute_keyword(**case)
  8. print(result)

run,运行结果如下

看,在执行用例里面的输入【飞人】成功,然后调用公共用例里面的,执行输入【路飞】成功

最后又回到执行用例里的点击su百度一下成功

核心代码的讲述解束

 

核心部分全部代码如下:


  
  1. from common.getconf import Config
  2. from common.getcase import ReadCase
  3. from basefactory.browseroperator import BrowserOperator
  4. from basefactory.webdriveroperator import WebdriverOperator
  5. class Factory(object):
  6. def __init__(self):
  7. self.con = Config()
  8. self.con_fun = dict(self.con.items('Function'))
  9. """
  10. 浏览器操作对象
  11. """
  12. self.browser_opr = BrowserOperator()
  13. """
  14. 网页操作对象
  15. """
  16. self.webdriver_opr = None
  17. def init_webdriver_opr(self, driver):
  18. self.webdriver_opr = WebdriverOperator(driver)
  19. def get_base_function(self, function_name):
  20. try:
  21. function = getattr(self.browser_opr, function_name)
  22. except Exception:
  23. try:
  24. function = getattr(self.webdriver_opr, function_name)
  25. except Exception:
  26. return False, '未找到注册方法[' + function_name + ']所对应的执行函数,请检查配置文件'
  27. return True, function
  28. def execute_keyword(self, **kwargs):
  29. """
  30. 工厂函数,用例执行方法的入口
  31. :param kwargs:
  32. :return:
  33. """
  34. try:
  35. keyword = kwargs['keyword']
  36. if keyword is None:
  37. return False, '没有keyword,请检查用例'
  38. except KeyError:
  39. return False, '没有keyword,请检查用例'
  40. _isbrowser = False
  41. try:
  42. function_name = self.con_fun[keyword]
  43. except KeyError:
  44. return False, '方法Key['+ keyword +']未注册,请检查用例'
  45. #获取基础类方法
  46. isOK, result = self.get_base_function(function_name)
  47. if isOK:
  48. function = result
  49. else:
  50. return isOK, result
  51. #执行基础方法,如打网点页、点击、定位、隐式等待 等
  52. isOK, result = function(**kwargs)
  53. #如果是打开网页,是浏览器初始化,需要将返回值传递给另一个基础类
  54. if '打开网页' == keyword and isOK:
  55. url = kwargs['locator']
  56. self.init_webdriver_opr(result)
  57. return isOK, '网页[' + url + ']打开成功'
  58. return isOK, result
  59. def init_common_case(self, cases):
  60. """
  61. :param kwargs:
  62. :return:
  63. """
  64. cases_len = len(cases)
  65. index = 0
  66. for case in cases:
  67. if case['keyword'] == '调用用例':
  68. xlsx = ReadCase()
  69. try:
  70. case_name = case['locator']
  71. except KeyError:
  72. return False, '调用用例没提供用例名,请检查用例'
  73. isOK, result = xlsx.get_common_case(case_name)
  74. if isOK and type([]) == type(result):
  75. isOK, result_1 = self.init_common_case(result) #递归检查公共用例里是否存在调用用例
  76. elif not isOK:
  77. return isOK, result
  78. list_rows = result[case_name]
  79. cases[index: index+1] = list_rows #将公共用例插入到执行用例中去
  80. index += 1
  81. if cases_len == index:
  82. return False, ''
  83. return True, cases
  84. # [{"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]}]
  85. def init_execute_case(self):
  86. print("----------初始化用例----------")
  87. xlsx = ReadCase()
  88. isOK, result = xlsx.readallcase()
  89. if not isOK:
  90. print(result)
  91. print("----------结束执行----------")
  92. exit()
  93. all_cases = result
  94. excu_cases = []
  95. for cases_dict in all_cases:
  96. for key, cases in cases_dict.items():
  97. isOK, result = self.init_common_case(cases)
  98. if isOK:
  99. cases_dict[key] = result
  100. else:
  101. cases_dict[key] = cases
  102. excu_cases.append(cases_dict)
  103. print("----------初始化用例完成----------")
  104. return excu_cases

 

ddt驱动代码

 

1、文件目录

执行用例代码目录 excutetest下新增一个test_caserun.py  ,先说,这里最好是以test开头定义执行文件

然后library里面放到ddt,数据驱动装饰器

2、导入库

在test_caserun.py里写代码


  
  1. import unittest
  2. from common.factory import Factory
  3. from common.log import mylog
  4. from library.ddt import ddt, data

unittest UT测试库,作自动化测试的都知道他是干啥的

Factory,这个框架的核心类

mylog,下一章实现的日志打印

ddt, data,数据驱动装饰器

2、代码分析

 


  
  1. @ddt
  2. class Test_caserun(unittest.TestCase):
  3. fac = Factory()
  4. isOK, excu_cases = fac.init_execute_case()
  5. @data(*excu_cases)
  6. def test_run(self, acases):
  7. for key, cases in acases.items():
  8. mylog.info('\n----------用例【%s】开始----------' % cases[0].get('sheet'))
  9. print('\n')
  10. for case in cases:
  11. isOK, result = self.fac.execute_keyword(**case)
  12. if isOK:
  13. print(result)
  14. mylog.info(result)
  15. else:
  16. mylog.error(result)
  17. raise Exception(result)
  18. 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只遍历了第一层,后面的我们代码里面写


  
  1. for key, cases in acases.items():
  2. mylog.info('\n----------用例【%s】开始----------' % cases[0].get('sheet'))
  3. print('\n')
  4. for case in cases:
  5. isOK, result = self.fac.execute_keyword(**case)
  6. if isOK:
  7. print(result)
  8. mylog.info(result)
  9. else:
  10. mylog.error(result)
  11. raise Exception(result)
  12. 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里导入


  
  1. import os
  2. import unittest
  3. from library.HTMLTestRunnerNew import HTMLTestRunner
  4. from common.getfiledir import CASEDIR, REPORTDIR

  os来处理路径 

unittest来Loadcase的代码文件

HTMLTestRunner来执行用例,输出报告

具体代码如下,这一段网上通用,我也懒懒不细说了直接上


  
  1. class Test_run(object):
  2. def __init__(self):
  3. self.suit = unittest.TestSuite()
  4. self.load = unittest.TestLoader()
  5. self.suit.addTest(self.load.discover(CASEDIR))
  6. self.runner = HTMLTestRunner(
  7. stream=open(os.path.join(REPORTDIR, 'report.html'), 'wb'),
  8. title='魂尾自动化工厂',
  9. description='唯a饭木领',
  10. tester='HUNWEI'
  11. )
  12. def excute(self):
  13. self.runner.run(self.suit)
  14. if __name__=="__main__":
  15. test_run = Test_run()
  16. test_run.excute()

init初始化用例,与报告对象

excute执行

添加工程主执行入口main

直接运行调试,还是在之前的excel用例基础上运行

运行结果

输出日志:

报告结果:

找到这个目录

打开这个html

好,整个框架完成了,只剩扩展一些边边角角的了

比如:下面打印日志,与发送邮件的,便不细说了,可以直接看代码学习学习,很简单的。

打印Log

相关代码


  
  1. import logging
  2. import os
  3. from logging.handlers import TimedRotatingFileHandler
  4. from common.getconf import Config
  5. from common.getfiledir import LOGDIR
  6. class Handlogging():
  7. @staticmethod
  8. def emplorlog():
  9. conf = Config()
  10. # set format
  11. formatter = logging.Formatter("%(asctime)s - %(name)s-%(levelname)s %(message)s")
  12. # create log set getlog level
  13. mylog = logging.getLogger('HunWei')
  14. mylog.setLevel(conf.get('LOG', 'level'))
  15. # create outputsteam set level
  16. sh = logging.StreamHandler()
  17. sh.setLevel(conf.get('LOG', 'level'))
  18. sh.setFormatter(formatter)
  19. mylog.addHandler(sh)
  20. #create file set level
  21. #fh = logging.FileHandler(os.path.join(LOGDIR,'test.log'), encoding='utf-8')
  22. log_path = os.path.join(LOGDIR, 'test')
  23. # interval 滚动周期,
  24. # when="MIDNIGHT", interval=1 表示每天0点为更新点,每天生成一个文件
  25. # backupCount 表示日志保存个数
  26. fh = TimedRotatingFileHandler(
  27. filename=log_path, when="D", backupCount=15, encoding='utf-8'
  28. )
  29. fh.suffix = "%Y-%m-%d.log"
  30. fh.setLevel(conf.get('LOG', 'level'))
  31. fh.setFormatter(formatter)
  32. mylog.addHandler(fh)
  33. return mylog
  34. mylog = Handlogging.emplorlog()

 

发送邮件

相关代码


  
  1. import smtplib
  2. import os
  3. from common.getconf import Config
  4. from email.mime.text import MIMEText
  5. from email.mime.multipart import MIMEMultipart
  6. from email.mime.application import MIMEApplication
  7. from common.getfiledir import REPORTDIR
  8. class Opr_email(object):
  9. def __init__(self):
  10. """
  11. 初始化文件路径与相关配置
  12. """
  13. self.conf = Config()
  14. all_path = []
  15. for maindir, subdir, file_list in os.walk(REPORTDIR):
  16. pass
  17. for filename in file_list:
  18. all_path.append(os.path.join(REPORTDIR, filename))
  19. self.filename = all_path[0]
  20. self.host = self.conf.get('email', 'host')
  21. self.port = self.conf.get('email', 'port')
  22. self.user = self.conf.get('email', 'user')
  23. self.pwd = self.conf.get('email', 'pwd')
  24. self.from_addr = self.conf.get('email', 'from_addr')
  25. self.to_addr = self.conf.get('email', 'to_addr')
  26. def get_email_host_smtp(self):
  27. """
  28. 连接stmp服务器
  29. :return:
  30. """
  31. self.smtp = smtplib.SMTP_SSL(host=self.host, port=self.port)
  32. self.smtp.login(user=self.user, password=self.pwd)
  33. def made_msg(self):
  34. """
  35. 构建一封邮件
  36. :return:
  37. """
  38. self.msg = MIMEMultipart()
  39. with open(self.filename, 'rb') as f:
  40. content = f.read()
  41. # 创建文本内容
  42. text_msg = MIMEText(content, _subtype='html', _charset='utf8')
  43. # 添加到多组件的邮件中
  44. self.msg.attach(text_msg)
  45. # 创建邮件的附件
  46. report_file = MIMEApplication(content)
  47. report_file.add_header('Content-Disposition', 'attachment', filename=str.split(self.filename, '\\').pop())
  48. self.msg.attach(report_file)
  49. # 主题
  50. self.msg['subject'] = '自动化测试报告'
  51. # 发件人
  52. self.msg['From'] = self.from_addr
  53. # 收件人
  54. self.msg['To'] = self.to_addr
  55. def send_email(self):
  56. """
  57. 发送邮件
  58. :return:
  59. """
  60. self.get_email_host_smtp()
  61. self.made_msg()
  62. self.smtp.send_message(self.msg, from_addr=self.from_addr, to_addrs=self.to_addr)

 

 

 

 

 

 


转载:https://blog.csdn.net/weixin_40331132/article/details/106482043
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场