三、填充我们的框架
设计主类
我们首先来实现我们的测试用例的主类设计,这个类主要用以被其他的测试用例继承,来实现一些每个测试用例都会做的事情,具体一点就是:
继承unittest,创建一个webdriver的实例,以及每次运行用例时打开和关闭浏览器。
可能之后还有更多这样的共性的事情会被放到测试主类,到时候我们就继续在测试主类里添加。
我们在之前预留的位置maincase里新建一个py文件,在里面写这个主类MainCase。
(稍微的介绍一下unittest框架,unittest会在正式的测试用例开始之前执行setup方法,在执行用例结束后会执行teardown方法,测试用例是指以test开头的方法,例如test_something这样的名字)
# -*- coding: utf-8 -*-
from selenium import webdriver
from operate.baseoperate import BaseOperate
import unittest
# 主测试类继承自测试框架unittest
class MainCase(unittest.TestCase):
# 声明一个webdriver
driver = webdriver
# 声明一个基础操作类base_operate
base_operate = BaseOperate
def setUp(self):
# 测试之前,启动浏览器,打开一个设定好的网址
self.driver = webdriver.Chrome()
self.driver.get("http://open.qq.com")
# 最大化窗口
self.driver.maximize_window()
# 传入webdriver,实例化这个类
self.base_operate = BaseOperate(self.driver)
def tearDown(self):
# 测试完毕的时候关闭浏览器
self.driver.quit()
if __name__ == '__main__':
unittest.main()
这样我们的主类就设计好了,之后我们写正式的testcase的时候,只需要继承这个类就可以了。
设计基础操作类和config文件
为了让正式的用例写的更加的爽,我们还需要封装一些基础操作方便调用,这个类放到我们预留的位置operate里,名字叫做BaseOperate。
为了让这个类可以随时调用,我们在前面提到的测试主类里,把webdriver的实例给他,具体的做法是,为BaseOperate这个类设计一个构造函数,在MainCase里传入其webdriver实例。
BaseOperate这个类的代码如下:
# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException
class BaseOperate():
# 声明一个webdriver类
operate_driver = webdriver.Chrome
def __init__(self, driver):
# 构造函数里将driver传入
self.operate_driver = driver
MainCase里修改一下,现在是这样:
# -*- coding: utf-8 -*-
from selenium import webdriver
from operate.baseoperate import BaseOperate
import unittest
# 主测试类继承自测试框架unittest
class MainCase(unittest.TestCase):
# 实例化一个webdriver
driver = webdriver.Chrome()
# 声明一个基础操作类base_operate
base_operate = BaseOperate
def setUp(self):
# 测试之前,启动浏览器,打开一个设定好的网址
self.driver.get("http://open.qq.com")
# 传入webdriver,实例化这个类
self.base_operate = BaseOperate(self.driver)
def tearDown(self):
# 测试完毕的时候关闭浏览器
self.driver.close()
if __name__ == '__main__':
unittest.main()
ok,接下来我们的用例只需要继承自主测试类,就可以直接用base_operate里封装的代码了(后续会上这一块的代码),现在我们要做的是把这个基础操作类填充一下。
想一下,我们要封装什么基础操作才会更加方便我们用例的书写?这个问题其实没有标准答案的,这里附上一些常用的做法。
代码如下:
# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
from selenium.common.exceptions import NoSuchElementException
from config.sysconfig import *
import sys
import time
class BaseOperate():
# 声明一个webdriver类
operate_driver = webdriver.Chrome
def __init__(self, driver):
# 构造函数里将driver传入
self.operate_driver = driver
def find_element(self, *loc):
"""
重封装的find方法,接受元祖类型的参数,默认等待元素5秒,寻找失败时自动截图
:param loc:元组类型,必须是(By.NAME, 'username')这样的结构
:return:元素对象webelement
"""
try:
webelement = WebDriverWait(self.operate_driver, 5).until(lambda x: x.find_element(*loc))
return webelement
except (TimeoutException, NoSuchElementException) as e:
#寻找失败时自动截图至指定目录sreenshot,截图名称为调用方法名(测试用例名)+ 时间戳 + png后缀
self.operate_driver.get_screenshot_as_file(SCREENSHOTURL + sys._getframe(1).f_code.co_name + time.strftime(ISOTIMEFORMAT, time.localtime(time.time()))+".png")
def click_element(self, *loc):
"""
重封装的click方法,将寻找和点击封装到一起,适用于点击次数不多的元素
:param loc:元组类型,必须是(By.NAME, 'username')这样的结构
:return:None
"""
try:
webelement = self.find_element(*loc)
webelement.click()
except (TimeoutException, NoSuchElementException) as e:
print ('Error details :%s' % (e.msg))
def is_page_has_text(self, text):
"""
判断当前页面是否存在指定的文字
:param text:字符串类型,要判断是否存在的文字
:return:布尔值,True代表存在,False代表不存在
"""
nowtime = time.time()
while self.operate_driver.page_source.find(text) < 0:
time.sleep(2)
if time.time() - nowtime >= 30000:
return False
return True
def switch_to_last_handles(self):
"""
在打开的窗口里选择最后一个
:return:None
"""
all_handles = self.operate_driver.window_handles
self.operate_driver.switch_to_window(all_handles[-1])
def switch_to_another_hanles(self, now_handle):
"""
只适用于打开两个窗口的情况,传入现在的窗口句柄后,选择另一个窗口
:param now_handle:现在的窗口句柄
:return:
"""
# 得到当前开启的所有窗口的句柄
all_handles = self.operate_driver.window_handles
# 获取到与当前窗口不一样的窗口
for handle in all_handles:
if handle != now_handle:
self.operate_driver.switch_to_window(handle)
def clear_and_sendkeys(self, sendtexts, *loc):
"""
先清除当前文本框内的文字再输入新的文字的方法
:param sendtexts:要输入的新的文字
:param loc:元组类型,必须是(By.NAME, 'username')这样的结构
:return:None
"""
try:
webelement = self.find_element(*loc)
webelement.clear()
webelement.send_keys(sendtexts)
except (TimeoutException, NoSuchElementException) as e:
print ('Error details :%s' % (e.msg))
这里重点说一下find的封装思路,之前说过,我们希望元素的位置和主代码分离,因此这里就用了selenium的find方法的另一种形式,
find_element (by ='id',value = None )
这样的结构,我们把元素按照ELEMENT = {'登录按钮': (By.ID, 'loginSub')}
,这样的结构存储在我们预定的config位置里,文件名就叫他emtconfig,之后元素的增加修改删除都在这里维护,当需要用到该元素的时候,就引入这个文件,例如这段代码:
# -*- coding: utf-8 -*-
from maincase.maincase import MainCase
from config.emtconfig import ELEMENT
class Test(MainCase):
def test_1(self):
qmarket_loginbtn = self.base_operate.find_element(*ELEMENT['QQ市场首页登录按钮'])
用于参考的emtconfig的内容:
# -*- coding: utf-8 -*-
from selenium.webdriver.common.by import By
# 页面元素
ELEMENT = {
#首页元素
'QQ市场首页登录按钮': (By.XPATH, "//*[@id='jmod_topbar']//*/a[text()='登录']"),
'选择使用账号登录按钮': (By.XPATH, "//*[@id='switcher_plogin']"),
#登录界面元素
'QQ号文本框': (By.ID, "u"),
'QQ密码文本框': (By.ID, "p"),
'QQ登录按钮': (By.ID, "login_button"),
#管理中心元素
'首页管理中心按钮': (By.XPATH, "//*[@id='jmod_topbar']//*/a[text()='管理中心']"),
'应用管理中心更新安装包按钮': (By.XPATH, "/html/body//*/a[text()='更新安装包']"),
#应用详情页元素
'点击上传按钮': (By.XPATH, "//*[@id='j-apk-box']//*/p[text()='更新安装包']"),
'应用更新说明文本框': (By.XPATH, "//*[@id='j-apk-box']//*/textarea"),
'提交审核按钮': (By.ID, "j-submit-btn"),
'确认提交审核': (By.ID, "j-confirm-yes"),
}
为了知道为什么元素不能被寻找到,在用例执行的时候到底发生了什么,所以我们希望当寻找失败时可以有个截图方便回溯当时的环境状况,因此我们用了py的异常机制,在寻找失败时,执行except下的方法,也就是截图方法,我在config的位置新建了一个sysconfig的文件,里面用于存放一些系统的信息,例如截图的位置或者全局样式等等,我们的截图的方法就用了这个位置。
sysconfig现在的内容:
# -*- coding: utf-8 -*-
# 截图路保存径,绝对路径,也可以用相对路径
SCREENSHOTURL = 'C:/Users/zyj/PycharmProjects/uitest/sreenshot/'
# 时间样式
ISOTIMEFORMAT = '%Y%m%d%H%M%S'
设计逻辑操作类
这个是用于封装一些常用的逻辑操作或者跳转路径的操作的类,预留的位置operate里,名字叫做BusinessOperate。因为是和业务强相关,所以就不举例了,大约的实现方法是:
# -*- coding: utf-8 -*-
from operate.baseoperate import BaseOperate
class BusinessOperate(BaseOperate):
pass
因为没有实际方法,所以直接pass了,诸位真正用的时候可以自己根据业务需要补充代码。
设计工具类
这个类主要是非selenium的内容,但是用例也会用到的操作,例如打开windows窗口选择文件上传,发送email等,位置位于util里,这里上前文提到的两个实例。
封装打开win窗口的操作,类名为W32Operate:
# -*- coding: utf-8 -*-
import win32gui
import win32con
import sys #要重新载入sys。因为 Python 初始化后会删除 sys.setdefaultencoding 这个方法
reload(sys)
sys.setdefaultencoding('utf-8')
class W32Operate:
def win_upload(self, filepath):
"""
上传操作时,打开win的上传弹框,选取指定的文件
:param filepath: 上传的文件的路径
:return: None
"""
dialog = win32gui.FindWindow('#32770', u'打开') # 对话框
print (dialog)
#win32gui.SetForegroundWindow(dialog)
ComboBoxEx32 = win32gui.FindWindowEx(dialog, 0, 'ComboBoxEx32', None)
ComboBox = win32gui.FindWindowEx(ComboBoxEx32, 0, 'ComboBox', None)
Edit = win32gui.FindWindowEx(ComboBox, 0, 'Edit', None) # 上面三句依次寻找对象,直到找到输入框Edit对象的句柄
button = win32gui.FindWindowEx(dialog, 0, 'Button', u'打开(&O)') # 确定按钮Button
win32gui.SendMessage(Edit, win32con.WM_SETTEXT, None, filepath) # 往输入框输入绝对地址
win32gui.SendMessage(dialog, win32con.WM_COMMAND, 1, button) # 按button
封装email的操作,类名为EmailOperate(smtp的信息写在了sysconfig里,另外,如果你是采用我之后提到的pytest+allure+jenkins的方法的话,邮件的发送无需在框架内实现,这里写一下仅供参考):
# -*- coding: utf-8 -*-
from email.mime.text import MIMEText
from email.header import Header
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email import encoders
from config.sysconfig import *
import smtplib
class EmailOperate:
def send_mail_with_file(self, filepath, filename, subject):
"""
发送一个带有附件(测试报告)的邮件,邮件的配置项来自sysconfig
:param filepath: 报告文件的路径
:param filename: 报告文件的名称
:param subject: 邮件标题
:return: None
"""
# 邮件对象:
msg = MIMEMultipart()
msg['From'] = "UIAutoTester"
msg['To'] = "you"
msg['Subject'] = Header(subject, 'utf-8').encode()
# 邮件正文是MIMEText:
msg.attach(MIMEText('请查收自动化测试结果,见附件', 'plain', 'utf-8'))
# 添加附件:
with open(filepath, 'rb') as f:
# 设置附件的MIME和文件名:
mime = MIMEApplication(open(filepath, 'rb').read())
# 加上必要的头信息:
mime.add_header('Content-Disposition', 'attachment', filename=filename)
mime.add_header('Content-ID', '<0>')
mime.add_header('X-Attachment-Id', '0')
# 把附件的内容读进来:
mime.set_payload(f.read())
# 用Base64编码:
encoders.encode_base64(mime)
# 添加到MIMEMultipart:
msg.attach(mime)
server = smtplib.SMTP(smtp_server, 587)
server.starttls()
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
用例的组织和管理
我们之前预留了testsuite的位置,现在我们可以在这个位置新建我们自定义的测试用例集,使用unittest框架的话,用例的组织可以用三种方法,addTest(添加具体测试用例至用例集),makeSuite(加载某个类下的所有测试用例至用例集),以及discover(加载某个路径下的所有类里的所有测试用例至用例集),三种方法的示范代码如下:
addTest:
# coding = utf-8
import unittest
suite = unittest.TestSuite()
suite.addTest(你的测试用例类名('该类名下的测试用例方法名'))
if __name__=='__main__':
#执行用例
runner=unittest.TextTestRunner()
runner.run(suite)
makeSuite:
# coding = utf-8
import unittest
suite = unittest.TestSuite(unittest.makeSuite(你的测试用例类名))
if __name__=='__main__':
#执行用例
runner=unittest.TextTestRunner()
runner.run(suite)
discover:
# coding = utf-8
import unittest
suite = unittest.TestLoader().discover("你的测试用例类的路径")
if __name__=='__main__':
#执行用例
runner=unittest.TextTestRunner()
runner.run(suite)
生成测试报告
传统的和unittest框架配套的报告模块是HTMLTestRunner,使用方法也很简单,首先是下载,把下载的文件放到...\Python27\Lib目录下,然后用的时候import一下就行。
我们可以把代码写到testsuite文件里的最下面,报告的位置就用我们预留的testreport位置,例如:
if __name__=='__main__':
#执行测试
filename = config.ReportUrl + time.strftime(config.ISOTIMEFORMAT, time.localtime(time.time())) + 'result.html'
fp = file(filename, 'wb')
runner = HTMLTestRunner.HTMLTestRunner(
stream=fp,
title='YCG Test Report',
description='YcgkfsApp All Testcase'
)
runner.run(suite)
这样执行完了testsuite之后,就会在testreport目录下生成一个html文件格式的报告,结合之前写的email方法,就可以把报告发出去了,这一段就不举例了,因为后面会介绍一个更加简便的方法去做这件事。
生成的报告界面如下: