精华内容
下载资源
问答
  • 前面 Appium 自动化测试框架大概发的都差不多了,那么接下来分享有关 Appium 自动化测试框架的综合实践,想必有些小伙伴都等不及了吧! 1、driver配置封装 kyb_caps.yaml 配置表 ,主要是一些配置信息的封装。 代码...

    Time will tell.

    前面 Appium 自动化测试框架大概发的都差不多了,那么接下来分享有关 Appium 自动化测试框架的综合实践,想必有些小伙伴都等不及了吧!

    1、driver配置封装

    kyb_caps.yaml 配置表 ,主要是一些配置信息的封装。

    代码:

    platformName: Android
    #模拟器
    platformVersion: 5.1.1
    deviceName: 127.0.0.1:62025
    
    #mx4真机
    #platformVersion: 5.1
    #udid: 750BBKL22GDN
    #deviceName: MX4
    
    appname: kaoyan3.1.0.apk
    noReset: False
    unicodeKeyboard: True
    resetKeyboard: True
    
    appPackage: com.tal.kaoyan
    appActivity: com.tal.kaoyan.ui.activity.SplashActivity
    ip: 127.0.0.1
    port: 4723
    

    desired_caps.py,主要是用来读取配置文件的信息的封装。

    代码:

    # coding=utf-8
    # 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行
    # 2.注释:包括记录创建时间,创建人,项目名称。
    # 3.导入模块
    
    from appium import webdriver
    import yaml
    import logging
    import logging.config
    import os
    
    CON_LOG='../config/log.conf'
    logging.config.fileConfig(CON_LOG)
    logging=logging.getLogger()
    
    def appium_desired():
        with open('../config/kyb_caps.yaml','r',encoding='utf-8') as file:
            data=yaml.load(file)
    
        desired_caps={}
        desired_caps['platformName']=data['platformName']
        desired_caps['platformVersion']=data['platformVersion']
        desired_caps['deviceName']=data['deviceName']
    
        base_dir = os.path.dirname(os.path.dirname(__file__))
        app_path = os.path.join(base_dir, 'app', data['appname'])
        desired_caps['app']=app_path
    
        desired_caps['appPackage']=data['appPackage']
        desired_caps['appActivity']=data['appActivity']
        desired_caps['noReset']=data['noReset']
    
    
        desired_caps['unicodeKeyboard']=data['unicodeKeyboard']
        desired_caps['resetKeyboard']=data['resetKeyboard']
    
        logging.info('start app...')
        driver=webdriver.Remote('http://'+str(data['ip'])+':'+str(data['port'])+'/wd/hub',desired_caps)
        driver.implicitly_wait(8)
        return driver
    
    if __name__ == '__main__':
        appium_desired()
    
        # with open('../config/kyb_caps.yaml', 'r', encoding='utf-8') as file:
        #     data = yaml.load(file)
        #
        # base_dir=os.path.dirname(os.path.dirname(__file__))
        # print(os.path.dirname(__file__))
        # print(base_dir)
        #
        # app_path=os.path.join(base_dir,'app',data['appname'])
        # print(app_path)
    

    相对路径符号含义

    • “.” 表示当前目录

    • “…” 表示当前目录的上一级目录。

    • “./” 表示当前目录下的某个文件或文件夹,视后面跟着的名字而定

    • “…/” 表示当前目录上一级目录的文件或文件夹,视后面跟着的名字而定。

    2、基类封装

    baseView.py,主要是一些元素定位方法的封装。

    代码:

    # coding=utf-8
    # 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行
    # 2.注释:包括记录创建时间,创建人,项目名称。
    # 3.导入模块
    
    class BaseView(object):
        def __init__(self,driver):
            self.driver=driver
    
        def find_element(self,*loc):
            return self.driver.find_element(*loc)
    
        def find_elements(self,*loc):
            return self.driver.find_elements(*loc)
    
        def get_window_size(self):
            return self.driver.get_window_size()
    
        def swipe(self,start_x, start_y, end_x, end_y, duration):
            return self.driver.swipe(start_x, start_y, end_x, end_y, duration)
    

    3、common 公共模块封装

    公共方法封装 : common_fun.py,主要是一些公共方法的封装。

    代码:

    # coding=utf-8
    # 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行
    # 2.注释:包括记录创建时间,创建人,项目名称。
    # 3.导入模块
    
    from kyb_testProject.baseView.baseView import BaseView
    from kyb_testProject.common.desired_caps import appium_desired
    from selenium.common.exceptions import NoSuchElementException
    import logging
    from selenium.webdriver.common.by import By
    import time,os
    import csv
    
    class Common(BaseView):
        cancelBtn=(By.ID,'android:id/button2')
        skipBtn=(By.ID,'com.tal.kaoyan:id/tv_skip')
        wemedia_cacel=(By.ID,'com.tal.kaoyan:id/view_wemedia_cacel')
    
    
        def check_cancelBtn(self):
            logging.info('==========check_cancelBtn=========')
            try:
                cancelBtn = self.driver.find_element(*self.cancelBtn)
            except NoSuchElementException:
                logging.info('no cancelBtn')
            else:
                cancelBtn.click()
    
        def check_skipBtn(self):
            logging.info('=========check skipBtn=============')
    
            try:
                skipBtn = self.driver.find_element(*self.skipBtn)
            except NoSuchElementException:
                logging.info('no skipBtn')
            else:
                skipBtn.click()
    
        def get_size(self):
            x = self.driver.get_window_size()['width']
            y = self.driver.get_window_size()['height']
            return x, y
    
        def swipeLeft(self):
            logging.info('swipeLeft')
            l = self.get_size()
            x1 = int(l[0] * 0.9)
            y1 = int(l[1] * 0.5)
            x2 = int(l[0] * 0.1)
            self.swipe(x1, y1, x2, y1, 1000)
    
        def getTime(self):
            self.now=time.strftime("%Y-%m-%d %H_%M_%S")
            return self.now
    
        def getScreenShot(self,module):
            time=self.getTime()
            image_file=os.path.dirname(os.path.dirname(__file__))+'/screenshots/%s_%s.png' %(module,time)
    
            logging.info('get %s screenshot' %module)
            self.driver.get_screenshot_as_file(image_file)
    
        def check_market_ad(self):
            logging.info('====check_market_ad====')
            try:
                element=self.driver.find_element(*self.wemedia_cacel)
            except NoSuchElementException:
                pass
            else:
                logging.info('close market ad')
                element.click()
    
        def get_csv_data(self,csv_file,line):
            logging.info('=====get_csv_data======')
            with open(csv_file,'r',encoding='utf-8-sig') as file:
                reader=csv.reader(file)
                for index,row in enumerate(reader,1):
                    if index==line:
                        return row
    
    if __name__ == '__main__':
        # driver=appium_desired()
        # com=Common(driver)
        # com.check_cancelBtn()
        # # com.check_skipBtn()
        # com.swipeLeft()
        # com.getScreenShot('startApp')
    
        list = ["这", "是", "一个", "测试", "数据"]
        # for i in range(len(list)):
            # print(i, list[i])
    
        list1 = ["这", "是", "一个", "测试", "数据"]
        # for index, item in enumerate(list1):
        #     print(index, item)
    

    好了,今天的分享就到这里。如果你对更多内容、Python自动化软件测试、面试题感兴趣的话可以加入我们175317069一起学习。群里会有各项测试学习资源发放,更有行业深潜多年的技术人分析讲解。

    祝你能成为一名优秀的软件测试工程师!

    欢迎【点赞】、【评论】、【关注】~

    Time will tell.(时间会证明一切)

    展开全文
  • 本篇章紧接上一篇继续来分享关于 Appium 自动化测试框架综合实践案例代码。框架所需要的代码实现都已基本完成。 data数据封装 1.使用背景 在实际项目过程中,我们的数据可能是存储在一个数据文件中,如txt,excel、...

    Time will tell.

    本篇章紧接上一篇继续来分享关于 Appium 自动化测试框架综合实践案例代码。框架所需要的代码实现都已基本完成。

    data数据封装

    1.使用背景

    在实际项目过程中,我们的数据可能是存储在一个数据文件中,如txt,excelcsv文件类型。我们可以封装一些方法来读取文件中的数据来实现数据驱动。


    2.案例

    将测试账号存储在account.csv文件,如下:

    account.csv

    hg2018 hg2018
    hg2019 zxw2019
    666 222

    代码:


    3.enumerate()简介

    enumerate()是 python 的内置函数

    • enumerate在字典上是枚举、列举的意思。
    • 对于一个可迭代的(iterable)、可遍历的对象(如列表、字符串),enumerate将其组成一个索引序列,利用它可以同时获得索引和值。
    • enumerate多用于在 for 循环中得到计数。


    4.enumerate()使用

    如果对一个列表,既要遍历索引又要遍历元素时,首先可以这样写:

    上述方法有些累赘,利用enumerate()会更直接和优美:


    数据读取方法封装

    数据读取方法也属于公共方法,这里我们首先实现一下,然后将其封装到里边即可。

    1.数据读取方法实现的参考代码

    import csv
    
         def get_csv_data(csv_file,line):
    
            with open(csv_file, 'r', encoding='utf-8-sig') as file:
    
                reader=csv.reader(file)
    
                for index, row in enumerate(reader,1):
    
                    if index == line:
    
                        return row
    
     
    
        csv_file='../data/account.csv'
    
        data=get_csv_data(csv_file,3)
    
        print(data)
    


    2.封装

    将其封装在公共方法中,在其他地方用到的时候,直接导入调用即可。


    utf-8 与 utf-8-sig 两种编码格式的区别

    UTF-8 以字节为编码单元,它的字节顺序在所有系统中都是一样的,没有字节序的问题,也因此它实际上并不需要 BOM(“ByteOrder Mark”) 。但是 UTF-8 with BOM 即 utf-8-sig 需要提供 BOM 。


    config 文件配置

    各种配置文件都放在这个目录下。


    日志文件配置 ,主要是一些日志信息的配置。

    log.config

    参考代码:

    [loggers]
    keys=root,infoLogger
    
    [logger_root]
    level=DEBUG
    handlers=consoleHandler,fileHandler
    
    [logger_infoLogger]
    handlers=consoleHandler,fileHandler
    qualname=infoLogger
    propagate=0
    
    [handlers]
    keys=consoleHandler,fileHandler
    
    [handler_consoleHandler]
    class=StreamHandler
    level=INFO
    formatter=form02
    args=(sys.stdout,)
    
    [handler_fileHandler]
    class=FileHandler
    level=INFO
    formatter=form01
    args=('../logs/runlog.log', 'a')
    
    [formatters]
    keys=form01,form02
    
    [formatter_form01]
    format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s
    
    [formatter_form02]
    format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s
    

    测试用例封装

    这里举例演示一下:封装注册和登录两个测试用例。


    1.测试用例执行开始结束操作封装

    测试用例执行开始和结束的封装,其他模块用到直接导入,调用即可。

    myunit.py

    参考代码:

    # coding=utf-8
    # 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行
    # 2.注释:包括记录创建时间,创建人,项目名称。
    # 3.导入模块
    
    import unittest
    from kyb_testProject.common.desired_caps import appium_desired
    import logging
    from time import sleep
    
    class StartEnd(unittest.TestCase):
        def setUp(self):
            logging.info('=====setUp====')
            self.driver=appium_desired()
    
        def tearDown(self):
            logging.info('====tearDown====')
            sleep(5)
            self.driver.close_app()
    


    2.注册用例

    开始注册用例代码逻辑的实现。

    test_register.py

    参考代码:

    # coding=utf-8
    # 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行
    # 2.注释:包括记录创建时间,创建人,项目名称。
    # 3.导入模块
    
    from kyb_testProject.common.myunit import StartEnd
    from kyb_testProject.businessView.registerView import RegisterView
    import logging,random,unittest
    
    class RegisterTest(StartEnd):
        def test_user_register(self):
            logging.info('======test_user_register======')
            r=RegisterView(self.driver)
    
            username = 'bjhg2019' + 'fly' + str(random.randint(1000, 9000))
            password = 'bjhg2020' + str(random.randint(1000, 9000))
            email = 'bjhg' + str(random.randint(1000, 9000)) + '@163.com'
    
            self.assertTrue(r.register_action(username,password,email))
    
    if __name__ == '__main__':
        unittest.main()
    


    3.登录用例

    开始登录用例代码逻辑的实现。

    test_login.py

    参考代码:

    # coding=utf-8
    # 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行
    # 2.注释:包括记录创建时间,创建人,项目名称。
    # 3.导入模块
    
    from kyb_testProject.common.myunit import StartEnd
    from kyb_testProject.businessView.loginView import LoginView
    import unittest
    import logging
    
    class TestLogin(StartEnd):
        csv_file='../data/account.csv'
    
        @unittest.skip('test_login_zxw2018')
        def test_login_zxw2018(self):
            logging.info('======test_login_zxw2018=====')
            l=LoginView(self.driver)
            data=l.get_csv_data(self.csv_file,2)
    
            l.login_action(data[0],data[1])
            self.assertTrue(l.check_loginStatus())
    
        # @unittest.skip('skip test_login_zxw2017')
        def test_login_zxw2017(self):
            logging.info('======test_login_zxw2017=====')
            l=LoginView(self.driver)
            data = l.get_csv_data(self.csv_file, 1)
    
            l.login_action(data[0], data[1])
            self.assertTrue(l.check_loginStatus())
    
        @unittest.skip('test_login_error')
        def test_login_error(self):
            logging.info('======test_login_error=====')
            l = LoginView(self.driver)
            data = l.get_csv_data(self.csv_file, 3)
    
            l.login_action(data[0], data[1])
            self.assertTrue(l.check_loginStatus(),msg='login fail!')
    
    if __name__ == '__main__':
        unittest.main()
    

    好了,至此, Appium 自动化测试框架就基本差不多完成了,下一篇就会讲到执行测试用例,生成测试报告。如果你对更多内容、 Python 自动化软件测试、面试题感兴趣的话可以加入我们175317069一起学习。群里会有各项测试学习资源发放,更有行业深潜多年的技术人分析讲解。

    祝你能成为一名优秀的软件测试工程师!

    欢迎【点赞】、【评论】、【关注】~

    Time will tell.(时间会证明一切)

    展开全文
  • 如果你对更多内容、Python自动化软件测试、面试题感兴趣的话可以加入我们175317069一起学习。群里会有各项测试学习资源发放,更有行业深潜多年的技术人分析讲解。 祝你能成为一名优秀的软件测试工程师! 欢迎【点赞...

    Time will tell.

    续上一章节内容完毕后,本章节将来继续分享执行测试用例和生成测试报告。好了, 话不多说,直接进入正题。

    BSTestRunner测试报告


    1、BSTestRunner下载地址:https://github.com/easonhan007/HTMLTestRunner


    2、下载后,引入项目中即可。关于这部分有兴趣的也可来看一个小实例:https://github.com/hongduhong/HTMLTestRunner-BSTestRunner


    3、run.py,执行所有测试用例,这个是所有框架的入口。

    参考代码:

    # coding=utf-8
    # 1.先设置编码,utf-8可支持中英文,如上,一般放在第一行
    # 2.注释:包括记录创建时间,创建人,项目名称。
    # 3.导入模块
    
    import unittest
    from BSTestRunner import BSTestRunner
    import time,logging
    import sys
    
    path='D:\\kyb_testProject\\'
    sys.path.append(path)
    
    test_dir='../test_case'
    report_dir='../reports'
    
    discover=unittest.defaultTestLoader.discover(test_dir,pattern='test_login.py')
    
    now=time.strftime('%Y-%m-%d %H_%M_%S')
    report_name=report_dir+'/'+now+' test_report.html'
    
    with open(report_name,'wb') as f:
        runner=BSTestRunner(stream=f,title='Kyb Test Report',description='kyb Android app test report')
        logging.info('start run test case...')
        runner.run(discover)
    

    注意:pattern参数可以控制运行不同模块的用例,如下所示表示运行指定路径以test开头的模块。

    discover = unittest.defaultTestLoader.discover(test_dir, pattern='test*.py')
    


    4、Bat 批处理执行测试

    前面脚本开发阶段都是用 pycharm IDE 工具来运行脚本,但是当我们的脚本开发完成后,还每次打开 IDE 来执行自动化测试就不合理了。不仅每次打开比较麻烦,而且 pycharm 内存资源占用比较 “感人” ,这样非常影响执行效率。针对这种情况,可以使用 cmd 命令或者封装为bat批处理脚本来运行。

    启动 appium 服务,启动appium服务通过批量处理脚本。

    start_appium.bat


    执行测试用例,说到底就是通过批量脚本执行框架入口文件run.py

    run.bat


    执行之前需要在run.py脚本添加如下内容:

    import sys
    
    path='D:\\kyb_testProject\\'
    
    sys.path.append(path)
    

    项目在 IDE(Pycharm) 中运行和我们在 cmd 中运行的路径是不一样的,在 pycharm 中运行时, 会默认 pycharm 的目录 + 工程所在目录为运行目录。

    而在 cmd 中运行时,会以工程目录所在目录来运行。在import包时会首先从pythonPATH的环境变量中来查看包,如果没有你的PYTHONPATH中所包含的目录没有工程目录的根目录,那么你在导入不是同一个目录下的其他工程中的包时会出现import错误。

    脚本编码格式必须为 utf-8 。


    5、自动化测试平台

    开发完测试脚本后,也使用了bat批处理来封装了启动 Appium 服务和运行测试用例。但仍不够自动化,比如我想每天下班时自动跑一下用例,或者当研发打了新包后自动开始运行测试脚本测试新包,那么该如实现?

    持续集成(Continuous integration)

    持续集成是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成,简称 CI 。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。


    Jenkins简介

    Jenkins 是一个开源软件项目,是基于 Java 开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。

    下载地址:https://jenkins.io/download/
    

    下载后安装到指定的路径即可,默认启动页面为localhots:8080,如果 8080 端口被占用无法打开,可以进入到 jenkins 安装目录,找到jenkins.xml配置文件打开,修改如下代码的端口号即可:

    <arguments>-Xrs -Xmx256m -Dhudson.lifecycle=hudson.lifecycle.WindowsServiceLifecycle -jar "%BASE%\jenkins.war" --httpPort=8080 --webroot="%BASE%\war"</arguments>
    


    构建触发器

    • 触发远程构建:如果你想通过访问一个特殊的预定义 URL 来触发新构建,请启用此选项;

    • Build after other projects are built:在其他项目触发的时候触发,里面有分为三种情况,也就是其他项目构建成功、失败、或者不稳定的时候触发项目;

    • Build periodically 定时构建;

    • GitHub hook trigger for GITScm polling,根源Git的源码更新来触发构建;

    • Poll SCM:定时检查源码变更(根据SCM软件的版本号),如果有更新就checkout最新code下来,然后执行构建动作。


    jenkins定时构建语法

    这是其实就是 corn 表达式,几分钟就可以掌握,有兴趣的可以百度一下,花费几分钟简单的学习一下。

    * * * * *
    

    五颗星的中间,用空格隔开:

    • 第一个 * 表示分钟,取值0~59
    • 第二个 * 表示小时,取值0~23
    • 第三个 * 表示一个月的第几天,取值1~31
    • 第四个 * 表示第几月,取值1~12
    • 第五个 * 表示一周中的第几天,取值0~7,其中0和7代表的都是周日


    使用案例

    每天下午下班前18点定时构建一次:
    
    0 18 * * *
    
    每天早上8点构建一次:
    
    0 8 * * *
    
    每30分钟构建一次:
    
    H/30 * * * *
    

    好了,以上就是本章全部内容了。如果你对更多内容、Python自动化软件测试、面试题感兴趣的话可以加入我们175317069一起学习。群里会有各项测试学习资源发放,更有行业深潜多年的技术人分析讲解。

    祝你能成为一名优秀的软件测试工程师!

    欢迎【点赞】、【评论】、【关注】~

    Time will tell.(时间会证明一切)

    展开全文
  • 可能会利用一周的时间,我们来写一个Appium自动化框架的搭建, 从0到1,跟着小鱼一起,完善Android 的自动化框架体系。 框架模式:PO 语言:python3.7 + Appium 1.17 框架功能 ・业务功能封装 ・测试用例封装 ・测试...

    框架背景

    可能会利用一周的时间,我们来写一个Appium自动化框架的搭建,
    从0到1,跟着小鱼一起,完善Android 的自动化框架体系。
    框架模式:PO
    语言:python3.7 + Appium 1.17

    框架功能

    ・业务功能封装
    ・测试用例封装
    ・测试包管理
    ・截图处理
    ・日志获取
    ・报告生成
    ・断言处理
    ・数据驱动
    ・数据配置

    框架视图

    在这里插入图片描述

    了解了框架的构成,那么接下来的几天,我们根据设计好的框架结构,
    来完善框架的代码内容。
    Appium自动化框架,全部教程,请查看《appium自动化框架从0到1》
    敲黑板:
    跟着小鱼,学完框架系列教程后,小鱼会把源码贡献出来,在github上自取哦~~

    展开全文
  • appium自动化框架搭建

    2020-12-15 16:47:07
    需要用到的软件如下: jdk-8u121-window(32位的就下载32位的,64位的就下载64位的)。 2.Android-sdk_r24.4.1-windows.zip ...6.Appium-python-Client 7.pycharm JDK安装: JDK下载,可从官网下载或者云盘下载,地
  • Time will tell. ...Appium 可以说是做 App 目前流行的自动化框架,它的主要优势是支持 android 和 ios ,另外脚本语言也是支持 Java 和 Python ,而在很多招聘要求上也看到了这项技能。还有就是,目前 5G 时代已.
  • python+unnitest+appium自动化测试框架: 以下是本篇文章正文内容,下面案例可供参考 一、python是什么? Python由荷兰数学和计算机科学研究学会的Guido van Rossum 于1990 年代初设计,作为一门叫做ABC语言的替代...
  • Python Appium移动端app自动化测试框架

    千次阅读 2019-05-28 16:52:54
    最近有时间把前面写的Python UI自动化脚本转换成了适用于App的测试,整体架构没多少变化,先看整体架构 先从入口说起: (1)config.ini:运行前进行基本的配置,配置文件,由于是借鉴的UI框架,所以里面的有些...
  • appium自动化框架(1)

    千次阅读 2019-09-27 16:57:52
    Appium v1.14.1(不能低于1.6.3) selenium:3.141.0 测试设备:Android 5.1.1 Python:3.6 测试App:考研帮Android app V3.1.0 工程目录: 二、测试场景 启动APP 三、参考代码 desired_caps.py from appi.....
  • appium自动化框架(2)

    2019-09-29 09:52:12
    Appium v1.14.1(不能低于1.6.3) selenium:3.141.0 测试设备:Android 5.1.1 Python:3.6 测试App:考研帮Android app V3.1.0 工程目录: 二、测试场景 1 启动APP 2封装公共类 三、参考代码 BaseView.py ...
  • 终端输入"appium-doctor",若全部项显示对勾则安装成功,nesessary的按道理都需要安装,上图除了error是xcode的版本不对,这个是ios的ui自动化一定要安装的,但是此次针对安卓版本就没有安装 2、配置两个特殊的环境...
  • 1.基本框架 所有公司的框架大不相同,上图只是基本框架 2.yaml语言 YAML 是一种简洁的非标记语言。YAML以数据为中心,使用空白,缩进,分行组织数据,从而使得表示更加简洁易读。 由于实现简单,解析成本很低...
  • 不过宏哥经过一段时间的准备,appium的自动化测试框架完善的差不多了,那么接下来宏哥继续给小伙伴和童鞋们分享有关Appium自动化测试框架综合实践。想必小伙伴们有点等不及了吧! driver配置封装 kyb_caps.yaml ...
  • Appium是一个开源测试自动化测试框架,可用于原生、混合和移动web应用程序测试,它使用Webdriver协议驱动IOS、Android和Windows应用程序。Appium4.3版本之后,自带UI自动化测试工具—UIAutomator,在手写脚本过程中...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 546
精华内容 218
关键字:

pythonappium自动化框架

python 订阅