结构介绍
之前分享过一篇安卓UI测试,但是没有实现数据与代码分离,后期维护成本较高,所以最近抽空优化了一下。
不想看文章得可以直接去Github,欢迎拍砖
大致结构如下:
结构.png
-
testyaml管理用例,实现数据与代码分离,一个模块一个文件夹
-
public 存放公共文件,如读取配置文件、启动appium服务、读取Yaml文件、定义日志格式等
-
page 存放最小测试用例集,一个模块一个文件夹
-
results 存放测试报告及失败截图
report.png
-
logs 存放日志
logs.png
logdetail.png
- testcase 存放测试用例
- runtest.py 运行所有测试用例
yaml格式介绍
首先看下yaml文件的格式,之前也写过一点关于yaml语法学习的文章
testcase部分是重点,其中:
-
element_info:定位元素信息
-
find_type:属性,id、xpath、text、ids
-
operate_type: click、sendkeys、back、swipe_up 为back就是返回,暂时就四种
上面三个必填,operate_type必填!!!!!!
-
send_content:send_keys 时用到
-
index:ids时用到
-
times: 返回次数或者上滑次数
-
testinfo:
-
-
id:
cm001
-
title:
新增终端门店
-
execute:
1
-
testcase:
-
-
-
element_info:
客户
-
find_type:
text
-
operate_type:
click
-
-
-
element_info:
com.fiberhome.waiqin365.client:id/cm_topbar_tv_right
-
find_type:
id
-
operate_type:
click
-
-
-
element_info:
com.fiberhome.waiqin365.client:id/custview_id_singletv_inputtext
-
find_type:
ids
-
operate_type:
send_keys
-
send_content:
auto0205
-
index:
0
-
-
-
element_info:
-
find_type:
-
operate_type:
swipe_up
-
times:
1
-
-
-
element_info:
提交
-
find_type:
text
-
operate_type:
click
-
-
-
element_info:
-
find_type:
-
operate_type:
back
-
times:
1
代码部分
公共部分
个人觉得核心的就是公共部分,相当于建房子,公共部分搞好了,后面仅仅是调用即可,建房子把架子搭好,后面就添砖加瓦吧。
读取配置文件readconfig.py
设置日志格式logs.py
获取设备GetDevices.py
这几个通用的就不做介绍了
- 读取yaml文件 GetYaml.py
主要用来读取yaml文件
-
#coding=utf-8
-
#author='Shichao-Dong'
-
-
import sys
-
reload(sys)
-
sys.setdefaultencoding(
'utf8')
-
import yaml
-
import codecs
-
-
class getyaml:
-
def __init__(self,path):
-
self.path = path
-
-
def getYaml(self):
-
'''
-
读取yaml文件
-
:param path: 文件路径
-
:return:
-
'''
-
try:
-
f = open(self.path)
-
data =yaml.load(f)
-
f.close()
-
return data
-
except Exception:
-
print(
u"未找到yaml文件")
-
-
def alldata(self):
-
data =self.getYaml()
-
return data
-
-
def caselen(self):
-
data = self.alldata()
-
length = len(data[
'testcase'])
-
return length
-
-
def get_elementinfo(self,i):
-
data = self.alldata()
-
# print data['testcase'][i]['element_info']
-
return data[
'testcase'][i][
'element_info']
-
-
def get_findtype(self,i):
-
data = self.alldata()
-
# print data['testcase'][i]['find_type']
-
return data[
'testcase'][i][
'find_type']
-
-
def get_operate_type(self,i):
-
data = self.alldata()
-
# print data['testcase'][i]['operate_type']
-
return data[
'testcase'][i][
'operate_type']
-
-
def get_index(self,i):
-
data = self.alldata()
-
if self.get_findtype(i)==
'ids':
-
return data[
'testcase'][i][
'index']
-
else:
-
pass
-
-
def get_send_content(self,i):
-
data = self.alldata()
-
# print data['testcase'][i]['send_content']
-
if self.get_operate_type(i) ==
'send_keys':
-
return data[
'testcase'][i][
'send_content']
-
else:
-
pass
-
-
def get_backtimes(self,i):
-
data = self.alldata()
-
if self.get_operate_type(i)==
'back'
or self.get_operate_type(i)==
'swipe_up':
-
return data[
'testcase'][i][
'times']
-
else:
-
pass
-
-
def get_title(self):
-
data = self.alldata()
-
# print data['testinfo'][0]['title']
-
return data[
'testinfo'][
0][
'title']
-
- 启动appium服务 StartAppiumServer.py
主要是启动appium并返回端口port,这个port在下面的driver中需要
-
#coding=utf-8
-
#author='Shichao-Dong'
-
-
from logs
import log
-
import random,time
-
import platform
-
import os
-
from GetDevices
import devices
-
-
log = log()
-
dev = devices().get_deviceName()
-
-
class Sp:
-
def __init__(self, device):
-
self.device = device
-
-
def __start_driver(self, aport, bpport):
-
"""
-
:return:
-
"""
-
if platform.system() ==
'Windows':
-
import subprocess
-
subprocess.Popen(
"appium -p %s -bp %s -U %s" %
-
(aport, bpport, self.device), shell=
True)
-
-
def start_appium(self):
-
"""
-
启动appium
-
p:appium port
-
bp:bootstrap port
-
:return: 返回appium端口参数
-
"""
-
aport = random.randint(
4700,
4900)
-
bpport = random.randint(
4700,
4900)
-
self.__start_driver(aport, bpport)
-
-
log.info(
-
'start appium :p %s bp %s device:%s' %
-
(aport, bpport, self.device))
-
time.sleep(
10)
-
return aport
-
-
def main(self):
-
"""
-
:return: 启动appium
-
"""
-
return self.start_appium()
-
-
def stop_appium(self):
-
'''
-
停止appium
-
:return:
-
'''
-
if platform.system() ==
'Windows':
-
os.popen(
"taskkill /f /im node.exe")
-
-
if __name__ ==
'__main__':
-
s = Sp(dev)
-
s.main()
- 获取driver GetDriver.py
platformName、deviceName、appPackage、appActivity这些卸载配置文件config.ini文件中,可以直接通过readconfig.py文件读取获得。
appium_port有StartAppiumServer.py文件返回
-
s = Sp(deviceName)
-
appium_port = s.main()
-
-
def mydriver():
-
desired_caps = {
-
'platformName'
:platformName,
'deviceName'
:deviceName,
'platformVersion'
:platformVersion,
-
'appPackage'
:appPackage,
'appActivity'
:appActivity,
-
'unicodeKeyboard'
:True,
'resetKeyboard'
:True,
'noReset'
:True
-
}
-
try:
-
driver = webdriver.Remote(
'http://127.0.0.1:%s/wd/hub'%appium_port,desired_caps)
-
time.sleep(
4)
-
log.info(
'获取driver成功')
-
return driver
-
except
WebDriverException:
-
print
'No driver'
-
-
if __name_
_ ==
"__main__":
-
mydriver()
- 重新封装find等命令,BaseOperate.py
里面主要是一些上滑、返回、find等一些基础操作
-
#coding=utf-8
-
#author='Shichao-Dong'
-
-
from selenium.webdriver.support.ui
import WebDriverWait
-
from logs
import log
-
import os
-
import time
-
-
'''
-
一些基础操作:滑动、截图、点击页面元素等
-
'''
-
-
class BaseOperate:
-
def __init__(self,driver):
-
self.driver = driver
-
-
def back(self):
-
'''
-
返回键
-
:return:
-
'''
-
os.popen(
"adb shell input keyevent 4")
-
-
def get_window_size(self):
-
'''
-
获取屏幕大小
-
:return: windowsize
-
'''
-
global windowSize
-
windowSize = self.driver.get_window_size()
-
return windowSize
-
-
def swipe_up(self):
-
'''
-
向上滑动
-
:return:
-
'''
-
windowsSize = self.get_window_size()
-
width = windowsSize.get(
"width")
-
height = windowsSize.get(
"height")
-
self.driver.swipe(width/
2, height*
3/
4, width/
2, height/
4,
1000)
-
-
def screenshot(self):
-
now=time.strftime(
"%y%m%d-%H-%M-%S")
-
PATH =
lambda p: os.path.abspath(
-
os.path.join(os.path.dirname(__file__), p)
-
)
-
screenshoot_path = PATH(
'../results/screenshoot/')
-
self.driver.get_screenshot_as_file(screenshoot_path+now+
'.png')
-
-
def find_id(self,id):
-
'''
-
寻找元素
-
:return:
-
'''
-
exsit = self.driver.find_element_by_id(id)
-
if exsit :
-
return
True
-
else:
-
return
False
-
-
def find_name(self,name):
-
'''
-
判断页面是否存在某个元素
-
:param name: text
-
:return:
-
'''
-
findname =
"//*[@text='%s']"%(name)
-
exsit = self.driver.find_element_by_xpath(findname)
-
if exsit :
-
return
True
-
else:
-
return
False
-
-
def get_name(self,name):
-
'''
-
定位页面text元素
-
:param name:
-
:return:
-
'''
-
# element = driver.find_element_by_name(name)
-
# return element
-
-
findname =
"//*[@text='%s']"%(name)
-
try:
-
element = WebDriverWait(self.driver,
10).until(
lambda x: x.find_element_by_xpath(findname))
-
# element = self.driver.find_element_by_xpath(findname)
-
self.driver.implicitly_wait(
2)
-
return element
-
except:
-
self.screenshot()
-
log.error(
'未定位到元素:'+
'%s')%(name)
-
-
def get_id(self,id):
-
'''
-
定位页面resouce-id元素
-
:param id:
-
:return:
-
'''
-
try:
-
element = WebDriverWait(self.driver,
10).until(
lambda x: x.find_element_by_id(id))
-
# element = self.driver.find_element_by_id(id)
-
self.driver.implicitly_wait(
2)
-
return element
-
except:
-
self.screenshot()
-
log.error(
'未定位到元素:'+
'%s')%(id)
-
-
def get_xpath(self,xpath):
-
'''
-
定位页面xpath元素
-
:param id:
-
:return:
-
'''
-
try:
-
element = WebDriverWait(self.driver,
10).until(
lambda x: x.find_element_by_xpath(xpath))
-
# element = self.driver.find_element_by_xpath(xpath)
-
self.driver.implicitly_wait(
2)
-
return element
-
except:
-
self.screenshot()
-
log.error(
'未定位到元素:'+
'%s')%(xpath)
-
-
def get_ids(self,id):
-
'''
-
定位页面resouce-id元素组
-
:param id:
-
:return:列表
-
'''
-
try:
-
# elements = self.driver.find_elements_by_id(id)
-
elements = WebDriverWait(self.driver,
10).until(
lambda x: x.find_elements_by_id(id))
-
self.driver.implicitly_wait(
2)
-
return elements
-
except:
-
self.screenshot()
-
log.error(
'未定位到元素:'+
'%s')%(id)
-
-
def page(self,name):
-
'''
-
返回至指定页面
-
:return:
-
'''
-
i=
0
-
while i<
10:
-
i=i+
1
-
try:
-
findname =
"//*[@text='%s']"%(name)
-
self.driver.find_element_by_xpath(findname)
-
self.driver.implicitly_wait(
2)
-
break
-
except :
-
os.popen(
"adb shell input keyevent 4")
-
try:
-
findname =
"//*[@text='确定']"
-
self.driver.find_element_by_xpath(findname).click()
-
self.driver.implicitly_wait(
2)
-
except:
-
os.popen(
"adb shell input keyevent 4")
-
try:
-
self.driver.find_element_by_xpath(
"//*[@text='工作台']")
-
self.driver.implicitly_wait(
2)
-
break
-
except:
-
os.popen(
"adb shell input keyevent 4")
- Operate.py
我认为最关键的一步了,后面没有page都是调用这个文件进行测试,主要是根据读取的yaml文件,然后进行if...else...判断,根据对应的operate_type分别进行对应的click、sendkeys等操作
-
#coding=utf-8
-
#author='Shichao-Dong'
-
-
from GetYaml import getyaml
-
from BaseOperate import BaseOperate
-
-
class Operate:
-
def __init__(self,path,driver):
-
self.path = path
-
self.driver = driver
-
self.yaml = getyaml(
self.path)
-
self.baseoperate=BaseOperate(driver)
-
-
def check_operate_type(self):
-
''
'
-
读取yaml信息并执行
-
element_info:定位元素信息
-
find_type:属性,id、xpath、text、ids
-
operate_type: click、sendkeys、back、swipe_up 为back就是返回,暂时就三种
-
-
上面三个必填,operate_type必填!!!!!!
-
-
send_content:send_keys 时用到
-
index:ids时用到
-
times:
-
:return:
-
'
''
-
-
for i
in range(
self.yaml.caselen()):
-
if
self.yaml.get_operate_type(i) ==
'click':
-
if
self.yaml.get_findtype(i) ==
'text':
-
self.baseoperate.get_name(
self.yaml.get_elementinfo(i)).click()
-
elif
self.yaml.get_findtype(i) ==
'id':
-
self.baseoperate.get_id(
self.yaml.get_elementinfo(i)).click()
-
elif
self.yaml.get_findtype(i) ==
'xpath':
-
self.baseoperate.get_xpath(
self.yaml.get_elementinfo(i)).click()
-
elif
self.yaml.get_findtype(i) ==
'ids':
-
self.baseoperate.get_ids(
self.yaml.get_elementinfo(i))[
self.yaml.get_index(i)].click()
-
-
elif
self.yaml.get_operate_type(i) ==
'send_keys':
-
if
self.yaml.get_findtype(i) ==
'text':
-
self.baseoperate.get_name(
self.yaml.get_elementinfo(i)).send_keys(
self.yaml.get_send_content(i))
-
elif
self.yaml.get_findtype(i) ==
'id':
-
self.baseoperate.get_id(
self.yaml.get_elementinfo(i)).send_keys(
self.yaml.get_send_content(i))
-
elif
self.yaml.get_findtype(i) ==
'xpath':
-
self.baseoperate.get_xpath(
self.yaml.get_elementinfo(i)).send_keys(
self.yaml.get_send_content(i))
-
elif
self.yaml.get_findtype(i) ==
'ids':
-
self.baseoperate.get_ids(
self.yaml.get_elementinfo(i))[
self.yaml.get_index(i)].send_keys(
self.yaml.get_send_content(i))
-
-
elif
self.yaml.get_operate_type(i) ==
'back':
-
for n
in range(
self.yaml.get_backtimes(i)):
-
self.baseoperate.back()
-
-
elif
self.yaml.get_operate_type(i) ==
'swipe_up':
-
for n
in range(
self.yaml.get_backtimes(i)):
-
self.baseoperate.swipe_up()
-
-
def back_home(self):
-
''
'
-
返回至工作台
-
:return:
-
'
''
-
self.baseoperate.page(
'工作台')
-
公共部分的代码就介绍这么多,在编写这个框架的时候,大部分精力都花在这部分,所以个人觉得还是值得好好研究的
Page部分
page部分是最小用例集,一个模块一个文件夹,以客户为例,
目前写了两个用例,一个新增,一个排序,文件如下:
file.png
代码如下,非常的简洁,
-
import sys
-
reload(sys)
-
sys.setdefaultencoding(
'utf8')
-
import codecs,os
-
from public.Operate import Operate
-
from public.GetYaml import getyaml
-
-
PATH = lambda
p: os.path.abspath(
-
os.path.join(os.path.dirname(__file_
_), p)
-
)
-
yamlpath = PATH(
"../../testyaml/cm/cm-001addcm.yaml")
-
-
class AddcmPage:
-
-
def __init__(self,driver):
-
self.path = yamlpath
-
self.driver = driver
-
self.operate = Operate(
self.path,
self.driver)
-
-
def operateap(self):
-
self.operate.check_operate_type()
-
-
def home(self):
-
self.operate.back_home()
运行用例
这部分用了unittest,运行所有测试用例和生成报告。
一个模块一个用例,以客户为例:CmTest.py
-
from page.cm.CmAddcmPage
import AddcmPage
-
from page.cm.CmSortcmPage
import SortcmPage
-
-
-
from public.GetDriver
import mydriver
-
driver = mydriver()
-
-
import unittest,time
-
class Cm(unittest.TestCase):
-
-
def test_001addcm(self):
-
'''
-
新增客户
-
:return:
-
'''
-
add = AddcmPage(driver)
-
add.operateap()
-
add.home()
-
def test_002sortcm(self):
-
'''
-
客户排序
-
:return:
-
'''
-
sort = SortcmPage(driver)
-
sort.sortlist()
-
sort.home()
-
-
def test_999close(self):
-
driver.quit()
-
time.sleep(
10)
-
-
if __name__ ==
"__main__":
-
unittest.main()
首先从page层将需要运行的用例都import进来,然后用unittest运行即可。
如果想要运行所有的测试用例,需要用到runtest.py
-
import time,os
-
import unittest
-
import HTMLTestRunner
-
from testcase.CmTest
import Cm
-
-
-
def testsuit():
-
suite = unittest.TestSuite()
-
suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(Cm),
-
-
-
-
-
])
-
-
# runner = unittest.TextTestRunner(verbosity=2)
-
# runner.run(suite)
-
-
now=time.strftime(
"%y-%m-%d-%H-%M-%S")
-
PATH =
lambda p: os.path.abspath(
-
os.path.join(os.path.dirname(__file__), p)
-
)
-
dirpath = PATH(
"./results/waiqin365-")
-
-
filename=dirpath + now +
'result.html'
-
fp=open(filename,
'wb')
-
runner=HTMLTestRunner.HTMLTestRunner(stream=fp,title=
'waiqin365 6.0.6beta test result',description=
u'result:')
-
-
runner.run(suite)
-
fp.close()
-
-
if __name__ ==
"__main__":
-
testsuit()
这边的思路差不多,也是先导入再装入suite即可
总结
就目前而言,暂时算是实现了数据与用例的分离,但是yaml的编写要求较高,不能格式上出错。
同时也有一些其他可以优化的地方,如:
- 对弹窗的判断
- 断开后重连机制
- 失败后重跑机制
转载:https://blog.csdn.net/grl18840839630/article/details/109987870