结构介绍

之前分享过一篇安卓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: 返回次数或者上滑次数

  1. testinfo:
  2. - id: cm001
  3. title: 新增终端门店
  4. execute: 1
  5. testcase:
  6. -
  7. element_info: 客户
  8. find_type: text
  9. operate_type: click
  10. -
  11. element_info: com.fiberhome.waiqin365.client:id/cm_topbar_tv_right
  12. find_type: id
  13. operate_type: click
  14. -
  15. element_info: com.fiberhome.waiqin365.client:id/custview_id_singletv_inputtext
  16. find_type: ids
  17. operate_type: send_keys
  18. send_content: auto0205
  19. index: 0
  20. -
  21. element_info:
  22. find_type:
  23. operate_type: swipe_up
  24. times: 1
  25. -
  26. element_info: 提交
  27. find_type: text
  28. operate_type: click
  29. -
  30. element_info:
  31. find_type:
  32. operate_type: back
  33. times: 1

代码部分

公共部分

个人觉得核心的就是公共部分,相当于建房子,公共部分搞好了,后面仅仅是调用即可,建房子把架子搭好,后面就添砖加瓦吧。

读取配置文件readconfig.py
设置日志格式logs.py
获取设备GetDevices.py
这几个通用的就不做介绍了

  • 读取yaml文件 GetYaml.py
    主要用来读取yaml文件
  1. #coding=utf-8
  2. #author='Shichao-Dong'
  3. import sys
  4. reload(sys)
  5. sys.setdefaultencoding('utf8')
  6. import yaml
  7. import codecs
  8. class getyaml:
  9. def __init__(self,path):
  10. self.path = path
  11. def getYaml(self):
  12. '''
  13. 读取yaml文件
  14. :param path: 文件路径
  15. :return:
  16. '''
  17. try:
  18. f = open(self.path)
  19. data =yaml.load(f)
  20. f.close()
  21. return data
  22. except Exception:
  23. print(u"未找到yaml文件")
  24. def alldata(self):
  25. data =self.getYaml()
  26. return data
  27. def caselen(self):
  28. data = self.alldata()
  29. length = len(data['testcase'])
  30. return length
  31. def get_elementinfo(self,i):
  32. data = self.alldata()
  33. # print data['testcase'][i]['element_info']
  34. return data['testcase'][i]['element_info']
  35. def get_findtype(self,i):
  36. data = self.alldata()
  37. # print data['testcase'][i]['find_type']
  38. return data['testcase'][i]['find_type']
  39. def get_operate_type(self,i):
  40. data = self.alldata()
  41. # print data['testcase'][i]['operate_type']
  42. return data['testcase'][i]['operate_type']
  43. def get_index(self,i):
  44. data = self.alldata()
  45. if self.get_findtype(i)=='ids':
  46. return data['testcase'][i]['index']
  47. else:
  48. pass
  49. def get_send_content(self,i):
  50. data = self.alldata()
  51. # print data['testcase'][i]['send_content']
  52. if self.get_operate_type(i) == 'send_keys':
  53. return data['testcase'][i]['send_content']
  54. else:
  55. pass
  56. def get_backtimes(self,i):
  57. data = self.alldata()
  58. if self.get_operate_type(i)=='back' or self.get_operate_type(i)=='swipe_up':
  59. return data['testcase'][i]['times']
  60. else:
  61. pass
  62. def get_title(self):
  63. data = self.alldata()
  64. # print data['testinfo'][0]['title']
  65. return data['testinfo'][0]['title']
  • 启动appium服务 StartAppiumServer.py
    主要是启动appium并返回端口port,这个port在下面的driver中需要
  1. #coding=utf-8
  2. #author='Shichao-Dong'
  3. from logs import log
  4. import random,time
  5. import platform
  6. import os
  7. from GetDevices import devices
  8. log = log()
  9. dev = devices().get_deviceName()
  10. class Sp:
  11. def __init__(self, device):
  12. self.device = device
  13. def __start_driver(self, aport, bpport):
  14. """
  15. :return:
  16. """
  17. if platform.system() == 'Windows':
  18. import subprocess
  19. subprocess.Popen("appium -p %s -bp %s -U %s" %
  20. (aport, bpport, self.device), shell=True)
  21. def start_appium(self):
  22. """
  23. 启动appium
  24. p:appium port
  25. bp:bootstrap port
  26. :return: 返回appium端口参数
  27. """
  28. aport = random.randint(4700, 4900)
  29. bpport = random.randint(4700, 4900)
  30. self.__start_driver(aport, bpport)
  31. log.info(
  32. 'start appium :p %s bp %s device:%s' %
  33. (aport, bpport, self.device))
  34. time.sleep(10)
  35. return aport
  36. def main(self):
  37. """
  38. :return: 启动appium
  39. """
  40. return self.start_appium()
  41. def stop_appium(self):
  42. '''
  43. 停止appium
  44. :return:
  45. '''
  46. if platform.system() == 'Windows':
  47. os.popen("taskkill /f /im node.exe")
  48. if __name__ == '__main__':
  49. s = Sp(dev)
  50. s.main()
  • 获取driver GetDriver.py
    platformName、deviceName、appPackage、appActivity这些卸载配置文件config.ini文件中,可以直接通过readconfig.py文件读取获得。
    appium_port有StartAppiumServer.py文件返回
  1. s = Sp(deviceName)
  2. appium_port = s.main()
  3. def mydriver():
  4. desired_caps = {
  5. 'platformName':platformName,'deviceName':deviceName, 'platformVersion':platformVersion,
  6. 'appPackage':appPackage,'appActivity':appActivity,
  7. 'unicodeKeyboard':True,'resetKeyboard':True,'noReset':True
  8. }
  9. try:
  10. driver = webdriver.Remote('http://127.0.0.1:%s/wd/hub'%appium_port,desired_caps)
  11. time.sleep(4)
  12. log.info('获取driver成功')
  13. return driver
  14. except WebDriverException:
  15. print 'No driver'
  16. if __name__ == "__main__":
  17. mydriver()
  • 重新封装find等命令,BaseOperate.py
    里面主要是一些上滑、返回、find等一些基础操作
  1. #coding=utf-8
  2. #author='Shichao-Dong'
  3. from selenium.webdriver.support.ui import WebDriverWait
  4. from logs import log
  5. import os
  6. import time
  7. '''
  8. 一些基础操作:滑动、截图、点击页面元素等
  9. '''
  10. class BaseOperate:
  11. def __init__(self,driver):
  12. self.driver = driver
  13. def back(self):
  14. '''
  15. 返回键
  16. :return:
  17. '''
  18. os.popen("adb shell input keyevent 4")
  19. def get_window_size(self):
  20. '''
  21. 获取屏幕大小
  22. :return: windowsize
  23. '''
  24. global windowSize
  25. windowSize = self.driver.get_window_size()
  26. return windowSize
  27. def swipe_up(self):
  28. '''
  29. 向上滑动
  30. :return:
  31. '''
  32. windowsSize = self.get_window_size()
  33. width = windowsSize.get("width")
  34. height = windowsSize.get("height")
  35. self.driver.swipe(width/2, height*3/4, width/2, height/4, 1000)
  36. def screenshot(self):
  37. now=time.strftime("%y%m%d-%H-%M-%S")
  38. PATH = lambda p: os.path.abspath(
  39. os.path.join(os.path.dirname(__file__), p)
  40. )
  41. screenshoot_path = PATH('../results/screenshoot/')
  42. self.driver.get_screenshot_as_file(screenshoot_path+now+'.png')
  43. def find_id(self,id):
  44. '''
  45. 寻找元素
  46. :return:
  47. '''
  48. exsit = self.driver.find_element_by_id(id)
  49. if exsit :
  50. return True
  51. else:
  52. return False
  53. def find_name(self,name):
  54. '''
  55. 判断页面是否存在某个元素
  56. :param name: text
  57. :return:
  58. '''
  59. findname = "//*[@text='%s']"%(name)
  60. exsit = self.driver.find_element_by_xpath(findname)
  61. if exsit :
  62. return True
  63. else:
  64. return False
  65. def get_name(self,name):
  66. '''
  67. 定位页面text元素
  68. :param name:
  69. :return:
  70. '''
  71. # element = driver.find_element_by_name(name)
  72. # return element
  73. findname = "//*[@text='%s']"%(name)
  74. try:
  75. element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_xpath(findname))
  76. # element = self.driver.find_element_by_xpath(findname)
  77. self.driver.implicitly_wait(2)
  78. return element
  79. except:
  80. self.screenshot()
  81. log.error('未定位到元素:'+'%s')%(name)
  82. def get_id(self,id):
  83. '''
  84. 定位页面resouce-id元素
  85. :param id:
  86. :return:
  87. '''
  88. try:
  89. element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_id(id))
  90. # element = self.driver.find_element_by_id(id)
  91. self.driver.implicitly_wait(2)
  92. return element
  93. except:
  94. self.screenshot()
  95. log.error('未定位到元素:'+'%s')%(id)
  96. def get_xpath(self,xpath):
  97. '''
  98. 定位页面xpath元素
  99. :param id:
  100. :return:
  101. '''
  102. try:
  103. element = WebDriverWait(self.driver, 10).until(lambda x: x.find_element_by_xpath(xpath))
  104. # element = self.driver.find_element_by_xpath(xpath)
  105. self.driver.implicitly_wait(2)
  106. return element
  107. except:
  108. self.screenshot()
  109. log.error('未定位到元素:'+'%s')%(xpath)
  110. def get_ids(self,id):
  111. '''
  112. 定位页面resouce-id元素组
  113. :param id:
  114. :return:列表
  115. '''
  116. try:
  117. # elements = self.driver.find_elements_by_id(id)
  118. elements = WebDriverWait(self.driver, 10).until(lambda x: x.find_elements_by_id(id))
  119. self.driver.implicitly_wait(2)
  120. return elements
  121. except:
  122. self.screenshot()
  123. log.error('未定位到元素:'+'%s')%(id)
  124. def page(self,name):
  125. '''
  126. 返回至指定页面
  127. :return:
  128. '''
  129. i=0
  130. while i<10:
  131. i=i+1
  132. try:
  133. findname = "//*[@text='%s']"%(name)
  134. self.driver.find_element_by_xpath(findname)
  135. self.driver.implicitly_wait(2)
  136. break
  137. except :
  138. os.popen("adb shell input keyevent 4")
  139. try:
  140. findname = "//*[@text='确定']"
  141. self.driver.find_element_by_xpath(findname).click()
  142. self.driver.implicitly_wait(2)
  143. except:
  144. os.popen("adb shell input keyevent 4")
  145. try:
  146. self.driver.find_element_by_xpath("//*[@text='工作台']")
  147. self.driver.implicitly_wait(2)
  148. break
  149. except:
  150. os.popen("adb shell input keyevent 4")
  • Operate.py
    我认为最关键的一步了,后面没有page都是调用这个文件进行测试,主要是根据读取的yaml文件,然后进行if...else...判断,根据对应的operate_type分别进行对应的click、sendkeys等操作
  1. #coding=utf-8
  2. #author='Shichao-Dong'
  3. from GetYaml import getyaml
  4. from BaseOperate import BaseOperate
  5. class Operate:
  6. def __init__(self,path,driver):
  7. self.path = path
  8. self.driver = driver
  9. self.yaml = getyaml(self.path)
  10. self.baseoperate=BaseOperate(driver)
  11. def check_operate_type(self):
  12. '''
  13. 读取yaml信息并执行
  14. element_info:定位元素信息
  15. find_type:属性,id、xpath、text、ids
  16. operate_type: click、sendkeys、back、swipe_up 为back就是返回,暂时就三种
  17. 上面三个必填,operate_type必填!!!!!!
  18. send_content:send_keys 时用到
  19. index:ids时用到
  20. times:
  21. :return:
  22. '''
  23. for i in range(self.yaml.caselen()):
  24. if self.yaml.get_operate_type(i) == 'click':
  25. if self.yaml.get_findtype(i) == 'text':
  26. self.baseoperate.get_name(self.yaml.get_elementinfo(i)).click()
  27. elif self.yaml.get_findtype(i) == 'id':
  28. self.baseoperate.get_id(self.yaml.get_elementinfo(i)).click()
  29. elif self.yaml.get_findtype(i) == 'xpath':
  30. self.baseoperate.get_xpath(self.yaml.get_elementinfo(i)).click()
  31. elif self.yaml.get_findtype(i) == 'ids':
  32. self.baseoperate.get_ids(self.yaml.get_elementinfo(i))[self.yaml.get_index(i)].click()
  33. elif self.yaml.get_operate_type(i) == 'send_keys':
  34. if self.yaml.get_findtype(i) == 'text':
  35. self.baseoperate.get_name(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
  36. elif self.yaml.get_findtype(i) == 'id':
  37. self.baseoperate.get_id(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
  38. elif self.yaml.get_findtype(i) == 'xpath':
  39. self.baseoperate.get_xpath(self.yaml.get_elementinfo(i)).send_keys(self.yaml.get_send_content(i))
  40. elif self.yaml.get_findtype(i) == 'ids':
  41. self.baseoperate.get_ids(self.yaml.get_elementinfo(i))[self.yaml.get_index(i)].send_keys(self.yaml.get_send_content(i))
  42. elif self.yaml.get_operate_type(i) == 'back':
  43. for n in range(self.yaml.get_backtimes(i)):
  44. self.baseoperate.back()
  45. elif self.yaml.get_operate_type(i) == 'swipe_up':
  46. for n in range(self.yaml.get_backtimes(i)):
  47. self.baseoperate.swipe_up()
  48. def back_home(self):
  49. '''
  50. 返回至工作台
  51. :return:
  52. '''
  53. self.baseoperate.page('工作台')

公共部分的代码就介绍这么多,在编写这个框架的时候,大部分精力都花在这部分,所以个人觉得还是值得好好研究的

Page部分

page部分是最小用例集,一个模块一个文件夹,以客户为例,
目前写了两个用例,一个新增,一个排序,文件如下:

 
file.png

代码如下,非常的简洁,

  1. import sys
  2. reload(sys)
  3. sys.setdefaultencoding('utf8')
  4. import codecs,os
  5. from public.Operate import Operate
  6. from public.GetYaml import getyaml
  7. PATH = lambda p: os.path.abspath(
  8. os.path.join(os.path.dirname(__file__), p)
  9. )
  10. yamlpath = PATH("../../testyaml/cm/cm-001addcm.yaml")
  11. class AddcmPage:
  12. def __init__(self,driver):
  13. self.path = yamlpath
  14. self.driver = driver
  15. self.operate = Operate(self.path,self.driver)
  16. def operateap(self):
  17. self.operate.check_operate_type()
  18. def home(self):
  19. self.operate.back_home()

运行用例

这部分用了unittest,运行所有测试用例和生成报告。
一个模块一个用例,以客户为例:CmTest.py

  1. from page.cm.CmAddcmPage import AddcmPage
  2. from page.cm.CmSortcmPage import SortcmPage
  3. from public.GetDriver import mydriver
  4. driver = mydriver()
  5. import unittest,time
  6. class Cm(unittest.TestCase):
  7. def test_001addcm(self):
  8. '''
  9. 新增客户
  10. :return:
  11. '''
  12. add = AddcmPage(driver)
  13. add.operateap()
  14. add.home()
  15. def test_002sortcm(self):
  16. '''
  17. 客户排序
  18. :return:
  19. '''
  20. sort = SortcmPage(driver)
  21. sort.sortlist()
  22. sort.home()
  23. def test_999close(self):
  24. driver.quit()
  25. time.sleep(10)
  26. if __name__ == "__main__":
  27. unittest.main()

首先从page层将需要运行的用例都import进来,然后用unittest运行即可。
如果想要运行所有的测试用例,需要用到runtest.py

  1. import time,os
  2. import unittest
  3. import HTMLTestRunner
  4. from testcase.CmTest import Cm
  5. def testsuit():
  6. suite = unittest.TestSuite()
  7. suite.addTests([unittest.defaultTestLoader.loadTestsFromTestCase(Cm),
  8. ])
  9. # runner = unittest.TextTestRunner(verbosity=2)
  10. # runner.run(suite)
  11. now=time.strftime("%y-%m-%d-%H-%M-%S")
  12. PATH = lambda p: os.path.abspath(
  13. os.path.join(os.path.dirname(__file__), p)
  14. )
  15. dirpath = PATH("./results/waiqin365-")
  16. filename=dirpath + now +'result.html'
  17. fp=open(filename,'wb')
  18. runner=HTMLTestRunner.HTMLTestRunner(stream=fp,title='waiqin365 6.0.6beta test result',description=u'result:')
  19. runner.run(suite)
  20. fp.close()
  21. if __name__ =="__main__":
  22. testsuit()

这边的思路差不多,也是先导入再装入suite即可

总结

就目前而言,暂时算是实现了数据与用例的分离,但是yaml的编写要求较高,不能格式上出错。
同时也有一些其他可以优化的地方,如:

  • 对弹窗的判断
  • 断开后重连机制
  • 失败后重跑机制
    等等,后续可以根据需求进行优化
    最后再贴一下开源地址Github,有兴趣的小伙伴可以去看一下,欢迎拍砖
    备注:完成过程中参考了Louis-me 和 auto 这两个开源项目,感谢!!!

作者:迈阿密小白
链接:https://www.jianshu.com/p/00aff8435a92
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处

基于python+appium+yaml安卓UI自动化测试分享的更多相关文章

  1. python+appium+yaml安卓UI自动化测试分享

    一.实现数据与代码分离,维护成本较低,先看看自动化结构,大体如下: testyaml管理用例,实现数据与代码分离,一个模块一个文件夹 public 存放公共文件,如读取配置文件.启动appium服务. ...

  2. Python 基于python实现的http接口自动化测试框架(含源码)

    基于python实现的http+json协议接口自动化测试框架(含源码) by:授客 QQ:1033553122      欢迎加入软件性能测试交流 QQ群:7156436  由于篇幅问题,采用百度网 ...

  3. appium+python 【Mac】UI自动化测试封装框架流程简介 <一>

    为了多人之间更方便的协作,那么框架本身的结构和编写方式将变得很重要,因此每个团队都有适合自己的框架.如下本人对APP的UI自动化测试的框架进行进行了简单的汇总.主要目的是为了让团队中的其余人员接手写脚 ...

  4. ATOMac - 基于Python的Mac应用Ui自动化库

    ATOMacTest 一.缘 起 近期工作需要对一款Mac端应用实现常用功能的自动化操作,同事推荐ATOMac这款工具,这几天简单研究了下,同时也发现现网介绍ATOMac的资料非常有限,故在此记录下A ...

  5. appium+python+unittest+HTMLRunner编写UI自动化测试集

    简介 获取AppPackage和AppActivity 定位UI控件的工具 脚本结构 PageObject分层管理 HTMLTestRunner生成测试报告 启动appium server服务 以py ...

  6. 如何快速搭建基于python+appium的自动化测试环境

    首先申明本文是基本于Python与Android来快速搭建Appium自动化测试环境: 主要分为以下几个步骤: 前提条件: 1)安装与配置python环境,打开 Python官网,找到“Downloa ...

  7. appium+python 【Mac】UI自动化测试封装框架介绍 <五>---脚本编写(多设备)

    目的: 通过添加设备号,则自动给添加的设备分配端口,启动对应的appium服务.注意:为了方便,将共用一个配置文件. 1.公共的配置文件名称:desired_caps.yaml platformVer ...

  8. appium+python 【Mac】UI自动化测试封装框架介绍 <二>---脚本编写(单设备)

    1.单设备的执行很简单,平时可多见的是直接在config中进行配置并进行运行即可.如下: # coding=UTF- ''' Created on // @author: SYW ''' from T ...

  9. appium+python 【Mac】UI自动化测试封装框架介绍 <四>---脚本的调试

    优秀的脚本调试定位问题具备的特点: 1.方便调试. 2.运行报错后容易定位出现的问题. 3.日志的记录清晰 4.日志可被存储,一般测试结果的分析在测试之后会进行,那么日志的存储将会为后期的分析问题带来 ...

随机推荐

  1. Python基础-python数据类型之集合(四)

    集合 集合是一个无序的,不重复的数据组合,基本功能包括关系测试和消除重复元素. 集合对象还支持 union,intersection,difference和 sysmmetric difference ...

  2. Solidity类型Uint类型区分?

    1. Solidity中默认 Uint 也就是Uint256, 也就是 无符号 256位整数范围,即 2的 256次方 减一的 10进制范围, 预计大小为: 115792089237316195423 ...

  3. 写一份简单的webpack2 的配置文件,无比简单

    这是一份自己用到的webpack2的配置写法,从看webpack2开始,发现自己越来越懒了,现在html文件都不想自己写了,直接自己生成... 哈哈,这次是可以无比完美的导入css啦 开发的时候在命令 ...

  4. AC自动机——1 Trie树(字典树)介绍

    AC自动机——1 Trie树(字典树)介绍 2013年10月15日 23:56:45 阅读数:2375 之前,我们介绍了Kmp算法,其实,他就是一种单模式匹配.当要检查一篇文章中是否有某些敏感词,这其 ...

  5. background-clip 和 background-origin 的区别

    background-origin:指定绘制背景图片的起点. background-clip:是对背景图片的剪裁,指定背景图片的显示范围. 1.background-origin:padding  | ...

  6. 【机器学习】Octave 实现逻辑回归 Logistic Regression

    ex2data1.txt ex2data2.txt 本次算法的背景是,假如你是一个大学的管理者,你需要根据学生之前的成绩(两门科目)来预测该学生是否能进入该大学. 根据题意,我们不难分辨出这是一种二分 ...

  7. tensorflow学习之(一)预测一条直线y = 0.1x + 0.3

    #预测一条y = 0.1x + 0.3的直线 import tensorflow as tf import numpy as np #科学计算模块 ''' tf.random_normal([784, ...

  8. Python11/12--GIL/互斥锁/进程池

    GIL1.全局解释器锁? 锁就是线程里面那个锁 锁是为了避免资源竞争造成数据的错乱 2.python程序的执行过程? 1.启动解释器进程 python.exe 2.解析你的py文件并执行它 每个py程 ...

  9. 包含复杂函数的excel 并下载

    POI 版本: <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</a ...

  10. xml to xsd ; xsd to xml

    xml to xsd 工具网站 https://www.freeformatter.com/xsd-generator.html 示例xml <?xml version="1.0&qu ...