• 基于python+appium+yaml安卓UI自动化测试分享


    结构介绍

    之前分享过一篇安卓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的编写要求较高,不能格式上出错。
    同时也有一些其他可以优化的地方,如:

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


    作者:迈阿密小白
    链接:https://www.jianshu.com/p/00aff8435a92
    來源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    PAT-乙级-1008 数组元素循环右移问题
    PAT-乙级-1007 素数对猜想
    PAT-乙级-1006 换个格式输出整数
    PAT-乙级-1005 继续(3n+1)猜想
    PAT-乙级-1003 我要通过!
    PAT-乙级-1004 成绩排名
    PAT-乙级-1002 写出这个数
    PAT-乙级-1001 害死人不偿命的(3n+1)猜想
    PAT-甲级-1002-A+B for Polynomials
    【windows】共享文件夹设置
  • 原文地址:https://www.cnblogs.com/finer/p/11895165.html
Copyright © 2020-2023  润新知