精华内容
下载资源
问答
  • 终端输入"appium-doctor",若全部项显示对勾则安装成功,nesessary的按道理都需要安装,上图除了error是xcode的版本不对,这个是ios的ui自动化一定要安装的,但是此次针对安卓版本就没有安装 2、配置两个特殊的环境...

    1、基础环境搭建

    • 可根据网上教程先安装一些基础环境: 基础环境搭建连接
    • 环境校验:验证appium安装是否完成
      终端输入"appium-doctor",若全部项显示对勾则安装成功,nesessary的按道理都需要安装,上图除了error是xcode的版本不对,这个是ios的ui自动化一定要安装的,但是此次针对安卓版本就没有安装
      在这里插入图片描述

    2、配置两个特殊的环境变量:
    安装两个命令aapt+adb和配置环境变量

    • aapt:资源打包工具,在框架里面的作用获取到app的相关信息 getAppInfo.py
    • adb:沟通你的安卓设备,在框架里面的作用是获取到你手机的相关信息 getDevicesInfo.py

    在这里插入图片描述
    在这里插入图片描述

    踩坑记录:
    遇到的踩坑之处有每次配置完环境变量执行命令行source ~/.bash_profile以后只在当前窗口可以执行aapt,该窗口关闭后就又要应用source ~/.bash_profile

    主要因为我电脑安装了zsh,加载的是~/.zshrc这个文件,只要在这个文件加上source ~/.bash_profile就能用了。可查看:https://www.jianshu.com/p/c33ccc64f036

    3、环境安装完成后,需要安装appium客户端

    用于启动appium-sevier

    4、准备需要测试的app安装包

    安装包放置在框架中的apps目录下

    5、设置app相关信息

    想要appium自动操作app,就需要获取app的相关信息,框架可通过执行getAppInfo来自动获取app安装包的信息,然后自动填写至app.conf里面,也可手动填写在app.conf里面

    6、启动appium

    运行代码框架的时候一定要先启动appium,配置页面写上host和端口号,端口号主要是通过代码里面写的得来;appium启动起来以后。剩下的就是写代码的事情了

    7、元素定位问题
    框架中对元素定位的方法进行了二次封装,只是变得更简单,和原来的差别不大的,在Handle/positioning目录中

    展开全文
  • 前段时间由于公司需要做接口测试,之前大多都是利用postman等工具进行的接口测试,于是我决定自己写python的接口自动化测试框架,这套自动化框架目前已经基本完成了并且运用到项目中,于是进行一些总结,便于以后...

    Python unittest+requests接口自动化测试框架打包exe

    	大家好!一名刚毕业实习的菜鸟,也是我第一次发表文章。前段时间由于公司需要做接口测试,之前大多都是利用postman等工具进行的接口测试,于是我决定自己写python的接口自动化测试框架,这套自动化框架目前已经基本完成了并且运用到项目中,于是进行一些总结,便于以后回顾温习,有许多不完善的地方,也遇到了许多的问题,希望大神们多多指教!
    

    思路

    	首先,我们编写框架需要有一个思路,首先跟大家讲一下接口测试的原理,调用发送请求调用源代码,获取相应比对预期结果与实际结果。既然是框架肯定得有数据源,然后发送请求获取相应,再来比对预期与响应结果是否正确,然后生成测试报告,或者是加入一些其他的功能,比如发送邮件日志信息等等 。技术思路如下:
    	1.通过xlrd库读取Excel中的测试用例
    	2.然后利用ddt数据驱动,unittest单元测试框架+requests库发送请求执行测试,并且用assert方法断言
    	3.利用Python的HTMLTestRunner库生成测试报告
    	4.smtplib库发送邮件测试报告及测试用例
    	5.最后通过pyinstaller库打包成exe程序
    

    概览

    	接下来给大家讲一下框架,我的框架分成了6层
    	 common:读取Excel中的测试用例以及URL和参数
    	 excel_data:存放测试用例以及URL、参数和预期结果
    	 report:主要是存放生成的测试报告
    	 run:这里主要是执行程序
    	 send_email:这里存放着发送邮件的代码
    	 test_case: 这里主要存放发送请求以及断言的方法
    

    在这里插入图片描述

    详解

    1.首先给大家看一下data里面的测试用例(这里URL小编就不能透露给大家了):
    这里主要存放着编号,系统名称,模块名,接口名称,接口URL,请求方法,请求参数,以及预期结果,预期响应码,创建人,案例类型
    在这里插入图片描述
    2.然后给大家讲一下common层里面的东西
    这里主要是获取表格的行数列数,然后循环读取里面的数据,具体的细节注释在代码里面:
    在这里插入图片描述
    这里大家可以print一下data读取出来的内容
    在这里插入图片描述
    3.接下来就是讲解一下test_case里面的东西,也就是执行测试;这里引入了unittest框架,requests,ddt,json库,这些库的作用相信大家就不陌生了,不知道的可以去百度一下
    (1)这里的setUpClass初始化方法主要是执行登录请求,然后获取登录请求返回的Authorization认证,类似于cookie一类的东西
    (2)下面的方法主要是判断请求的方式,以及发送请求,获取相应的内容再来进行断言。这里要注意一下Excel中读取出来的状态码是float类型的,需要把它转为int类型再来断言
    在这里插入图片描述
    4.好了接下来是发送邮件的过程,发送邮件相信大家一定不陌生了,这里我主要是通过下标的方式来命名附件名称的
    在这里插入图片描述
    在这里插入图片描述
    5.接下来就是run执行层的东西,这里主要是运用unittest单元测试框架,大概意思是将整个test_case模块data_test类下面所有已test开头命名的方法全部添加到测试集,并实例化测试集;最后生成测试报告在指定文件夹report下。最后的话就是邮件的主题,内容,路径等一些变量了
    在这里插入图片描述
    6.最后呢通过pyinstaller库将我们的测试框架打包成exe程序
    (1)具体打包的步骤这里我整理了一下如下:
    a.首先下载pyinstaller,可以pip命令也可以下载压缩包,这里给大家推荐一个我下载过的网站 https://github.com/pyinstaller/pyinstaller
    b.在框架的目录下cmd命令行
    c. 输入"pyinstaller --icon=路径\图标名称.ico 主文件路径\主文件名.py"命令,-icon=的意思呢就是给打包的程序自定义一个图标,这里大家一定要注意只有指定大小的图标才能修改,给大家推荐个下载图标的网站https://www.easyicon.net/iconsearch//?s=addtime_DESC
    在这里插入图片描述
    d.打包好了呢,在刚刚cmd的目录下就会多出build,dist和一个.spec的文件,这时我们的exe程序就在我们的dist目录下
    e.最后一步呢将测试框架中其他的模块及.py文件放入生成的dist目录下(与exe程序的上一级目录同级)
    在这里插入图片描述
    在这里插入图片描述
    f.最后呢打开exe文件及运行测试框架,最后的测试结果和邮件发送结果呢,可以在report下查看测试报告,也可以在运行exe程序时默认打开的cmd命令行查看运行结果
    在这里插入图片描述

    总结

    	接下来咱们的测试框架就完毕了,自动化测试的话同理用这个框架也是可以的;有许多不完善的地方,也遇到了许多的问题,希望大神们多多指教!
    
    展开全文
  • 3. 流程自动化方案设计,例如,一键打包,自动开始测试,自动发送测试报告,自动运维部署上线等。 1. 主流Python开发IDE工具的基本使用,例如Pycharm 2. Python中模块,类和对象的具体代码讲解。 3. Sele...

              设计自动化测试框架的前提技能介绍

    1. 手工测试用例转换成自动化测试脚本的过程

    2. 能设计自动化测试框架,至少能够维护自动化测试框架。

    3. 流程自动化方案设计,例如,一键打包,自动开始测试,自动发送测试报告,自动运维部署上线等。

    1. 主流Python开发IDE工具的基本使用,例如Pycharm

    2. Python中模块,类和对象的具体代码讲解。

    3. Selenium 常见方法的二次封装。

    4. 自定义方法的封装和方法的调用-浏览器引擎类。

    5. Python读写配置文件介绍

    6. Python如何获取系统时间和时间的格式化处理。

    7. Python中常见字符串切割处理。

    8. Python自定义一个日志生成方法封装。

    9. Selenium中一个截图方法的封装。

    10. Python中继承的使用。

    通过介绍以上中级技能学习后,我们才可以,或者有能力去思考和动手去设计一个简单的自动化测试框架。
     ———————————————— 
                                            Python中类/函数/模块的简单介绍和方法调用

    新建一个包,然后在包下面新建一个demo.py文件。抄写以下代码到你的环境里,尝试运行下,看看有没有问题。

    关于Python中类和函数及方法的调用,我们写在这个demo.py文件,具体代码如下:

    # coding=utf-8
     
    class ClassA(object):
     
        string1  = "这是一个字符串。"
     
        def instancefunc(self):
            print ('这是一个实例方法。')
            print (self)
     
        @classmethod
        def classfunc(cls):
            print ('这是一个类方法。')
            print (cls)
     
        @staticmethod
        def staticfun():
            print ('这是一个静态方法。')
     
     
    test = ClassA()  # 初始化一个ClasssA的对象,test是类ClassA的实例对象
    test.instancefunc()  # 对象调用实例方法
     
    test.staticfun()  # 对象调用静态方法
     
    test.classfunc()  # 对象调用类方法
     
    print test.string1 # 对象调用类变量
     
    ClassA.instancefunc(test)  # 类调用实例方法,需要带参数,这里的test是一个对象参数
    ClassA.instancefunc(ClassA) # 类调用实例方法,需要带参数,这里的ClassA是一个类参数
    ClassA.staticfun() # 类调用静态方法
    ClassA.classfunc()  # 类调用类方法

    1. 类的定义,class开头的就表示这是一个类,小括号里面的,表示这个类的父类,涉及到继承,默认object是所有类的父类。python中定义类,小括号内主要有三种:1. 具体一个父类,2. object 3. 空白

    2. 函数或方法的定义, def开头就表示定义一个函数,方法包括,实例方法,类方法,静态方法,注意看类方法和静态方法定义的时候上面有一个@标记。

    3. 对象调用方法和类调用方法的使用。
     ———————————————— 

    最后,来说下python中的模块,在python中,你新建一个demo.py文件,那么一个.py文件可以说是一个模块,一个模块中,可以定义多个class,模块中也可以直接定义函数。和java一样,访问不同包下的类和方法之前,需要导入相关路径下的包。例如from selenium import webdriver  这个导入语句,我们知道webdriver这个接口是在selenium的模块下。

    本篇文章的学习目的,会用函数或者类来编写我们之前写过的脚本。

    以下用百度搜索举例,模仿上面用类调用实例的方法来写这个脚本,可能看起来比较啰嗦,但是代码多了,你就会体会到类的作用,注意这里self指的是当前BaiduSearch这个类本身:
     ———————————————— 
    # coding=utf-8
    import time
    from selenium import webdriver
     
     
    class BaiduSearch(object):
        driver = webdriver.Chrome()
        driver.maximize_window()
        driver.implicitly_wait(10)
     
        def open_baidu(self):
            self.driver.get("https://www.baidu.com")
            time.sleep(1)
     
        def test_search(self):
            self.driver.find_element_by_id('kw').send_keys("selenium")
            time.sleep(1)
            print self.driver.title
            try:
                assert 'selenium' in self.driver.title
                print ('Test pass.')
     
            except Exception as e:
                print ('Test fail.')
            self.driver.quit()
     
    baidu = BaiduSearch()
    baidu.open_baidu()
    baidu.test_search()

                          二次封装Selenium中几个方法

    本文来介绍,如何把常用的几个webdriver的方法封装到自己写的一个类中去,这个封装过程叫二次封装Selenium方法。我们把打开站点,浏览器前进和后退,关闭和退出浏览器这这个方法封装到一个新写的类中去。

    我们按照如下层次结构在PyCharm中新建两个包和两个.py文件:

     上图,baidu_search.py是我们编写测试脚本的python文件,具体测试代码写在这个文件。包test1下的basepage.py文件是这次我们介绍的二次封装selenium方法而新建的。这里提一下,python中默认规则,包名和文件名都是小写,类名称单词首字母大写,函数名称小写,多个字母下划线隔开。我们尽量遵守下这个不成文的约定
     ———————————————— 
    # coding=utf-8
     
     
    class BasePage(object):
        """
        主要是把常用的几个Selenium方法封装到BasePage这个类,我们这里演示以下几个方法
        back()
        forward()
        get()
        quit()
        """
        def __init__(self, driver):
            """
            写一个构造函数,有一个参数driver
            :param driver:
            """
            self.driver = driver
     
        def back(self):
            """
            浏览器后退按钮
            :param none:
            """
            self.driver.back()
     
        def forward(self):
            """
            浏览器前进按钮
            :param none:
            """
            self.driver.forward()
     
        def open_url(self, url):
            """
            打开url站点
            :param url:
            """
            self.driver.get(url)
     
        def quit_browser(self):
            """
            关闭并停止浏览器服务
            :param none:
            """
            self.driver.quit()

     

    上面的''''''是文档注释,一般在类的开始和函数的开始,用两个''''''括起来,简单描述下这个类或者函数的功能。

    接下来看看,我们脚本文件中如何去调用我们自己封装过的方法。

    baidu_search.py的内容如下:

    # coding=utf-8
    import time
    from selenium import webdriver
    from test1.basepage import BasePage
     
     
    class BaiduSearch(object):
     
        driver = webdriver.Chrome()
        driver.maximize_window()
        driver.implicitly_wait(10)
     
        basepage = BasePage(driver)
     
        def open_baidu(self):
            self.basepage.open_url("https://www.baidu.com")
            time.sleep(1)
     
        def test_search(self):
            self.driver.find_element_by_id('kw').send_keys("Selenium")
            time.sleep(1)
            self.basepage.back()
            self.basepage.forward()
            self.basepage.quit_browser()
     
    baidu = BaiduSearch()
    baidu.open_baidu()
    baidu.test_search()

    上面self.basepage的几行代码就是调用我们自己封装的方法去执行相关webdriver操作。这个只是一个简单的封装介绍,等后面,我们介绍了字符串切割,我们会再次介绍二次封装Selenium方法,例如将会把八大find_element方法封装到一个方法里去。

                   封装一个自己的类-浏览器引擎类

    我们知道了,如何去封装几个简单的Selenium方法到我们自定义的类,这次我们编写一个类,叫浏览器引擎类,通过更改一个字符串的值,利用if语句去判断和控制启动那个浏览器。这里我们暂时,支持三大浏览器(IE,Chrome,Firefox)。这里有一个前提条件,在基础篇中,启动三大浏览器的driver文件,检查下你的Python安装路径下有没有这三个driver插件,如果没有,请回到基础篇的如何启动火狐和IE浏览器文章去看看如何做。

          我们继续在test1这个包下新建一个browser_engine.py文件,然后在另外一个包下新建一个test.py文件去测试这个浏览器引擎类是否工作正常。这个浏览器引擎类,我们一开始写简单一点,只写启动浏览器。

    先看看browser_engine.py中的代码:
     ———————————————— 
    # coding=utf-8
    from selenium import webdriver
     
     
    class BrowserEngine(object):
        """
        定义一个浏览器引擎类,根据browser_type的值去,控制启动不同的浏览器,这里主要是IE,Firefox, Chrome
        """
        def __init__(self, driver):
            self.driver = driver
     
        browser_type = "IE"   # maybe Firefox, Chrome, IE
     
        def get_browser(self):
            """
            通过if语句,来控制初始化不同浏览器的启动,默认是启动Chrome
            :return: driver
            """
     
            if self.browser_type == 'Firefox':
                driver = webdriver.Firefox()
            elif self.browser_type == 'Chrome':
                driver = webdriver.Chrome()
            elif self.browser_type == 'IE':
                driver = webdriver.Ie()
            else: driver = webdriver.Chrome()
     
            driver.maximize_window()
            driver.implicitly_wait(10)
     
            return driver

    再看看test.py代码,进行测试,更改browser_engine.py中browser_type的值,去测试三大浏览器是否启动正常。

    # coding=utf-8
    import time
    from test1.browser_engine import BrowserEngine
     
     
    class TestBrowserEngine(object):
     
        def open_browser(self):
            browserengine = BrowserEngine(self)
            driver = browserengine.get_browser()
     
     
    tbe = TestBrowserEngine()
    tbe.open_browser()

    目前,自定义的浏览器引擎类到这里就封装好了,只支持打开不同浏览器,需要手动修改,引擎类中browser_type的值。看起来功能简单,但是我们只是需要学习这种做事的方式和思维,在下一个部分,框架设计的时候,我会再告诉大家如何去加强这个引擎类的功能,到时候去修改配置文件中的浏览器类型,而不是修改代码中的字段。通过修改配置文件,从而去打开不同浏览器,并开始测试相关脚本。


     ———————————————— 
                    Python读取配置文件内容

    本文来介绍下Python中如何读取配置文件。任何一个项目,都涉及到了配置文件和管理和读写,Python支持很多配置文件的读写,这里我们就介绍一种配置文件格式的读取数据,叫ini文件。Python中有一个类ConfigParser支持读ini文件。

    1. 在项目下,新建一个文件夹,叫config,然后在这个文件夹下新建一个file类型的文件:config.ini

    文件内容如下:
     ———————————————— 

    #  this is config file, only store browser type and server URL
    
    [browserType]
    #browserName = Firefox
    browserName = Chrome
    #browserName = IE
    
    [testServer]
    URL = https://www.baidu.com
    #URL = http://www.google.com
    
    
    2. 百度搜索一下,python中如何获取当前项目的根目录的相对路径

    这里采用:

    os.path.dirname(os.path.abspath('.'))
    

    3. 在另外一个包下新建一个测试类,用来测试读取配置文件是否正常。

    # coding=utf-8
    
    import configparser
    import os
    
    
    class TestReadConfigFile(object):
    
        def get_value(self):
            root_dir = os.path.dirname(os.path.abspath('.'))  # 获取项目根目录的相对路径
            print(root_dir)
    
            config = configparser.ConfigParser()
            file_path = os.path.dirname(os.path.abspath('.')) + '/configwj/config.ini'
            config.read(file_path)
            print(file_path)
    
            browser = config.get("browserType", "browserName")
            url = config.get("testServer", "URL")
    
            return(browser, url)  # 返回的是一个元组
    
    trcf = TestReadConfigFile()
    print(trcf.get_value())    注意config.ini 文件需要手动到文件路径修改名称加后缀

    你可以试试更改config.ini的内容,看看测试打印出来是不是你更改的东西,在配置文件一般#表示注释,你想要哪行配置代码起作用,你就把前面的#去除,并且在注释其他同一个区域。在ini文件中 中括号包裹起来的部分叫section,了解一下就可以。

     

                   Python获取系统时间和格式化时间显示

    前面一篇文章介绍了,Python如何读取config.ini文件,还有如何获取当前项目根目录相对路径写法。在实际项目的开发,获取项目根路径的相对路径写法是很有必要的,不要去是绝对路径。因为,你自己开发的一个项目,如果拷贝到别的电脑里,发现运行不了,需要更改很多文件的路径,那是不是很失败。本篇文章介绍如何去获取和打印格式化系统时间,我们很多时候,看到一些日志,前面都会记录年月日,时分秒,甚至毫秒,然后才是日志描述。这一篇文章,介绍时间获取和格式化时间,就是为了后面,如何写一个简单的日志类做铺垫的。

    在PyCharm下的一个包,右键,新建一个get_time.py文件,输入一下代码:

    这里提醒一个小技巧:在输入导入包的时候,有些包你没有安装,不是系统自带的,可能会遇到红色下划线,你需要鼠标悬停在这个红色下划线,然后在这行的左侧有一个小灯泡,鼠标点击这个小灯泡,一般会有import this xxx 或者install xxx,根据提示来导入包或者安装第三方插件。
     ———————————————— 
    # coding=utf-8
    import time
     
     
    class GetTime(object):
     
        def get_system_time(self):
            print (time.time()) # time.time()获取的是从1970年到现在的间隔,单位是秒
            print (time.localtime())
            new_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) # 格式化时间,按照 2017-04-15 13:46:32的格式打印出来
            print (new_time)
     
    gettime = GetTime()
    gettime.get_system_time()

                          Python中字符串切割操作

      本文来介绍Python中字符串切割操作,在Python中自带的一个切割方法split(),这个方法不带参数,就默认按照空格去切割字段,如果带参数,就按照参数去切割。为了演示切割效果,我们用百度搜索一个关键字,然后去找一共找到了多个结果的数字。

          例如,百度搜索“selenium”,查看找到了多少个结果,我们需要单独摘取出这个数字。
     ———————————————— 

    相关脚本代码如下:

    # coding=utf-8
    # __author__ = 'lenovo'
    
    import time
    from selenium import webdriver
    
    class GetSubString(object):
    
        def get_search_result(self):
            driver = webdriver.Chrome()
            driver.maximize_window()
            driver.implicitly_wait(6)
    
            driver.get('https://www.baidu.com')
            driver.find_element_by_id('kw').send_keys('selenium')
            time.sleep(5)
            search_result_string = driver.find_element_by_xpath("//*/div[@class='nums']").text
            print(search_result_string)
    
            new_string = search_result_string.split('约')[1]  # 第一次切割得到 xxxx个,[1]代表切割右边部分
            print(new_string)
            last_result = new_string.split('个')[0]  # 第二次切割,得到我们想要的数字 [0]代表切割参照参数的左边部分
            print(last_result)
    
    
    getstring = GetSubString()
    getstring.get_search_result()
    split的另一种用法,切割的同时直接指给对象 - 
    
    • a = '1234567890'

    •  
    • b,c = a.split('5')

    • print(b)

    • print(c)

                                       Python自定义封装一个简单的Log类  

    本文介绍如何写一个Python日志类,用来输出不同级别的日志信息到本地文件夹下的日志文件里。为什么需要日志输出呢,我们需要记录我们测试脚本到底做了什么事情,最好的办法是写事件监听。这个事件监听,对我们现在来说,还是有点复杂去理解,所以我这里,选择封装一个简单的日志类,同样达到这个效果。
    我们大概需要如下日志输出效果:
     ———————————————— 

    问题分析:
    我们需要封装一个简单的日志类,主要有以下内容:
    1. 生成的日志文件格式是 年月日时分秒.log
    2. 生成的xxx.log文件存储在项目根目录下Logs文件夹下
    3. 这个日志类,支持INFO,ERROR两种日志级别
    4. 日志里,每行日志输出,如上图,时间日期+执行类名称+日志级别+日志描述

    解决问题思路:
    1. 在根目录下新建一个Logs的文件夹,如何获取这个Log的相对路径,前面介绍过。
    2. 日志的保存命名,需要系统时间,前面也介绍过时间格式化输出
    3. Python中有一个logging模块来支持我们自定义封装一个新日志类。
    4. 在脚本里,初始化一个日志类的实例对象,然后去控制输出INFO还是ERROR日志信息。

    自定义日志类封装如下:logger.py,新建在test包下
     ———————————————— 

    # coding=utf-8
    # __author__ = 'lenovo'
    
    # _*_ coding: utf-8 _*_
    import logging
    import os.path
    import time
    
    
    class Logger(object):
    
        def __init__(self, logger):
            """
            指定保存日志的文件路径,日志级别,以及调用文件
                将日志存入到指定的文件中
            :param logger:
            """
            # 创建一个logger
            self.logger = logging.getLogger(logger)
            self.logger.setLevel(logging.DEBUG)
    
            # 创建一个handler,用于写入日志文件
            rq = time.strftime('%Y%m%d%H%M', time.localtime(time.time()))
            log_path = os.path.dirname(os.getcwd()) + '/test1/logs/'
            log_name = log_path + rq + '.log'
            fh = logging.FileHandler(log_name)
            fh.setLevel(logging.INFO)
    
            # 再创建一个handler,用于输出到控制台
            ch = logging.StreamHandler()
            ch.setLevel(logging.INFO)
    
            # 定义handler的输出格式
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
            fh.setFormatter(formatter)
            ch.setFormatter(formatter)
    
            # 给logger添加handler
            self.logger.addHandler(fh)
            self.logger.addHandler(ch)
    
        def getlog(self):
            return self.logger
    
    
    新写一个测试日志类,相关代码如下:
    # coding=utf-8
    # __author__ = 'lenovo'
    
    import time
    from selenium import webdriver
    from test1.logger import Logger
    
    mylogger = Logger(logger='TestMyLog').getlog()
    class TestMyLog(object):
    
        def print_log(self):
            driver = webdriver.Chrome()
            mylogger.info("打开浏览器")
            driver.maximize_window()
            mylogger.info("最大化浏览器。")
            driver.implicitly_wait(6)
    
            driver.get("https://www.baidu.com")
            mylogger.info("打开百度首页。")
            time.sleep(3)
            mylogger.info("暂停三秒")
            driver.quit()
            mylogger.info("关闭并推出浏览器。")
    
    testlog = TestMyLog()
    testlog.print_log()
    
    在PyCharm里运行下这个测试类,会在根目录下的Logs文件下,新建一个日志文件,打开效果如文章开头的日志输出图。好了,关于自定义封装log类,自己好好去读下代码,理解下每行代码的意思,日志类的封装和调用就介绍到这里。

                          把截图类方法封装到前面的BasePage.py

       本文介绍把截图类方法封装到BasePage.py文件里,这个文件是在前面Selenium方法二次封装文章里创建的,具体代码请到前面这篇里找。我们截图类写死了把截图图片保存到根目录下的Screenshots文件夹里,图片名称是当前系统时间,图片后缀名是png。新的BasePage.py内容如下:
     ———————————————— 
    # coding=utf-8
    import os
    import time
     
    from test.logger import Logger
     
    mylog = Logger(logger='BasePage').getlog()
    class BasePage(object):
        """
        主要是把常用的几个Selenium方法封装到BasePage这个类,我们这里演示以下几个方法
        back()
        forward()
        get()
        quit()
        """
     
        def __init__(self, driver):
            """
            写一个构造函数,有一个参数driver
            :param driver:
            """
            self.driver = driver
     
        def back(self):
            """
            浏览器后退按钮
            :param none:
            """
            self.driver.back()
     
        def forward(self):
            """
            浏览器前进按钮
            :param none:
            """
            self.driver.forward()
     
        def open_url(self, url):
            """
            打开url站点
            :param url:
            """
            self.driver.get(url)
     
        def quit_browser(self):
            """
            关闭并停止浏览器服务
            :param none:
            """
            self.driver.quit()
     
        def take_screenshot(self):
            """
            截图并保存在根目录下的Screenshots文件夹下
            :param none:
            """
            file_path = os.path.dirname(os.getcwd()) + '/Screenshots/'
            rq = time.strftime('%Y%m%d%H%M%S',time.localtime(time.time()))
            screen_name = file_path + rq + '.png'
            try :
                self.driver.get_screenshot_as_file(screen_name)
                mylog.info("开始截图并保存")
     
            except Exception as e:
                mylog.error("出现异常",format(e))
     
     主要看最后一个截图类方法的封装。

    测试类相关代码如下:

    # coding=utf-8
    import time
    from selenium import webdriver
     
    from test.basepage import BasePage
     
     
    class TestScreenshot(object):
        driver = webdriver.Chrome()
        driver.maximize_window()
        driver.implicitly_wait(10)
        basepage = BasePage(driver)
     
        def test_take_screen(self):
            self.basepage.open_url("https://www.baidu.com")
            time.sleep(1)
            self.basepage.take_screenshot()
            self.basepage.quit_browser()
     
    test = TestScreenshot()
    test.test_take_screen()

    运行后,可以在根目录下Screenshots文件夹里找到百度首页截图。

    本文就介绍了截图类方法添加到BasePage里,介绍了如何保存到根目录下的Screenshots文件夹。

    Python+Selenium中级篇之10-Python中的继承的使用

    本文开始介绍一个面向对象设计领域里,很常见的一种思想,继承。继承有很多好处,常听到的一句话就是,子类能够直接使用父类的方法,这样就可以减少子类代码量。其实,在自动化测试框架设计过程中,是很有必要把继承加入到你的测试脚本中去。接下来我们,简单写一个Python文件,来演示下继承的基本使用。

    1. 在test1包名下新建一个classA.py,这个就是我们的父类,里面有一个打开chrome浏览器和打开百度首页的方法。
     ———————————————— 
    #coding = utf-8
     
    from selenium import webdriver
    import time
     
     
    class ClassA(object):
     
        def open_baidu(self):
            driver = webdriver.Chrome()
            driver.maximize_window()
            driver.get("https://www.baidu.com")
            time.sleep(1)
            driver.quit()

    2. 在test2包下新建一个classB.py文件,这个继承classA.py里的CassA类。

    # coding = utf-8
    from test1.classA import ClassA
     
     
    class ClassB(ClassA):
     
        def test_inherit(self):
            self.open_baidu()
     
    test = ClassB()
    test.test_inherit()

    通过上面可以看出,只需要一句代码就可以实现ClassA中的方法,这个就是继承的好处,减少了很多代码的书写,提高代码的复用。在定义ClassB的时候就要指明ClassB的父类是ClassA. 继承相关的话题就介绍到这里,将在后面自动化框架设计会再次提到。

     

    展开全文
  • JeeSite 快速开发平台,不仅仅是一个后台开发框架,它是一个企业级快速开发解决方案,基于经典技术组合(Spring Boot、Spring MVC、Apache Shiro、MyBatis、Beetl、Bootstrap、AdminLTE)采用经典开发模式,让初学者...

    一.实战项目介绍

    1.项目介绍

    JeeSite 快速开发平台,不仅仅是一个后台开发框架,它是一个企业级快速开发解决方案,基于经典技术组合(Spring Boot、Spring MVC、Apache Shiro、MyBatis、Beetl、Bootstrap、AdminLTE)采用经典开发模式,让初学者能够更快的入门并投入到团队开发中去。在线代码生成功能,包括模块如:组织机构、角色用户、菜单及按钮授权、数据权限、系统参数、内容管理、工作流等。采用松耦合设计,模块增减便捷;界面无刷新,一键换肤;众多账号安全设置,密码策略;文件在线预览;消息推送;多元化第三方登录;在线定时任务配置;支持集群,支持SAAS;支持多数据源;支持读写分离、分库分表;支持微服务应用。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    2.环境部署

    JeeSite 是一款基于Spring Boot开发框架的项目。它的运行是需要依赖一些特定环境

    在这里插入图片描述

    2.1 部署MySQL

    在这里插入图片描述

    # 拉取docker镜像
    docker pull mysql
    # 实例化并启动mysql容器
    docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=ouyi1994 -p 3306:3306 mysql
    # 进入容器
    docker exec -it my_mysql bash
    # 验证mysql是否启动
    mysql -u root -p
    

    这时外界是无法访问MySQL的,还需要设置MySQL能接受任何方式连接。

    # 进入MySQL容器
    docker exec -it mysql容器id bash
    # 进入MySQL
    mysql -u root -p
    # 设置MySQL支持其他方式连接
    ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码'
    

    在这里插入图片描述

    2.2 部署Jdk

    参考前面章节

    2.3 部署maven

    maven主要是用于Java源码编译打包时需要。

    Linux下安装maven

    (1)下载maven安装包

    地址:http://maven.apache.org/download.cgi

    在这里插入图片描述

    (2)解压

    sudo tar -zxvf apache-maven-3.6.3-bin.tar.gz
    

    (3)配置环境变量

    vi /etc/profile
    
    export MAVEN_HOME=/usr/local/maven/apache-maven-3.6.3
    export PATH=$PATH:$MAVEN_HOME/bin
    

    (4)重新加载环境变量

    source /etc/profile
    

    (5)验证

    mvn -version
    

    (7)修改maven下载镜像源

    maven会使用坐标定位的方式,获取到我们代码中的第三方库。并联网下载。可以修改maven下载镜像源,来提供maven的下载速度。
    在这里插入图片描述

    进入maven安装目录下的config目录,修改setting.xml文件

    Windows下安装maven

    (1)下载安装包并解压

    (2)配置环境变量
    在这里插入图片描述

    (3)验证

    在这里插入图片描述

    (4)修改maven的下载镜像源

    修改apache-maven-3.6.3\conf下的settings.xml文件

    在这里插入图片描述

    <mirror>
        <id>alimaven</id>
        <name>aliyun maven</name>
        <url>http://maven.aliyun.com/nexus/content/qroups/public/</url>
        <mirrorOf>central</mirrorOf>
    </mirror>
    

    3.项目准备,调试,运行

    我们需要在本地将项目调试运行通过后,再做相关的持续集成操作

    3.1数据库准备

    (1)新建数据库

    create database jeesite
    

    (2)导入初始化数据到数据库

    第一步:项目文件中配置数据库相关信息

    在jeesite4\web\src\main\resources\config\application.yml文件中,填写数据库相关信息

    在这里插入图片描述

    第二步:执行初始化脚本

    jeesite4\web\bin目录下执行init-data.sh(Linux)或者init-data.bat(windows)脚本

    在这里插入图片描述

    第三步:查看数据库中是否已有初始数据

    在这里插入图片描述

    3.2 在IDEA中编译项目

    (1) 在maven中导入项目root模块下的pom.xml文件

    导入这个文件,会自动将整个项目导入到maven中

    在这里插入图片描述

    在这里插入图片描述

    (2)使用maven编译项目

    在这里插入图片描述

    3.3 在命令行编译项目

    注意:maven在命令行也可以编译项目,方式如下:

    进入项目的root目录后,执行命令‘mvn clean install’

    3.4 在IDEA中运行项目

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    3.5 在命令行运行项目

    进入项目的web目录,执行命令:‘mvn clean spring-boot:run’

    二.Tomcat部署持续交付实战

    1.实战介绍

    在这里插入图片描述
    在这里插入图片描述

    2.配置环境

    使用tomcat发布spring-boot项目(jeesite),需要以下环境。

    • jdk(前面已配置)
    • maven(前面已配置)
    • mysql(前面已配置)
    • tomcat

    配置tomcat

    1.下载tomcat安装包:http://tomcat.apache.org/download-80.cgi#8.0.50
    在这里插入图片描述

    2.将下载的tomcat传输到**/usr/local**/tomcat目录下解压

    tar -zxvf apache-tomcat-8.5.61.tar.gz
    # 添加权限,防止权限不够出错
    chmod -R 777 apache-tomcat-8.5.64/
    

    3.启动tomact

    cd /usr/local/tomcat/apache-tomcat-8.5.61/bin
    ./staup.sh
    

    4.检测:访问http://42.193.187.134:8080/,如果不能显示猫咪,则需要关闭防火墙

    tomcat常用方法介绍

    (1)启动,停止脚本

    在/usr/local/tomcat/apache-tomcat-8.5.64/bin目录下的startup.sh和shutdown.sh

    (2)日志文件

    在/usr/local/tomcat/apache-tomcat-8.5.64/logs目录下的catalina.out

    (3)配置文件

    在/usr/local/tomcat/apache-tomcat-8.5.64/conf目录下的server.xml

    在这里插入图片描述

    (4)发布项目

    将项目包拷贝到/usr/local/tomcat/apache-tomcat-8.5.64/webapps/ROOT目录下,用于发布

    3.Jenkins集成tomcat持续交付实战

    在这里插入图片描述
    在这里插入图片描述

    3.1编写pipeline脚本

    node('salve-mylinux2') {
        stage('同步源码') {
            sh"""
            	## 创建一个目录,用于存放拉取的源码
                mkdir jeesite || echo "the dir jeesite is exited"
            """
            dir("jeesite"){
                git credentialsId:'gitee_ouyi', branch:'master', url:'git@gitee.com:xxx/jeesite4.git' # 项目源码地址
            }
        }
    
        stage('maven编译打包') {
            sh '''
                . ~/.bash_profile
                cd jeesite
                export pwd=`pwd`
                ## 修改配置文件中,数据库的信息 
                cd web/src/main/resources/config
                sed -i "s/mysql_ip/${mysql_ip}/g" application.yml
                sed -i "s/mysql_port/${mysql_port}/g" application.yml
                sed -i "s/mysql_user/${mysql_user}/g" application.yml
                sed -i "s/mysql_pwd/${mysql_pwd}/g" application.yml
    
    			## 编译root模块
                cd $pwd/root
                mvn clean install -Dmaven.test.skip=true
    			
    			## 编译web模块
                cd $pwd/web
                mvn clean package spring-boot:repackage -Dmaven.test.skip=true -U
            '''
        }
    
        stage('停止 tomcat') {
            sh '''
                ## 停止tomcat的函数, 参数$1带入tomcat的路径$TOMCAT_PATH
                killTomcat()
                {
                    pid=`ps -ef|grep $1|grep java|awk '{print $2}'`
                    echo "tomcat Id list :$pid"
                    if [ "$pid" = "" ]
                    then
                      echo "no tomcat pid alive"
                    else
                      kill -9 $pid
                    fi
                }
                ## 停止Tomcat
                killTomcat $tomcat_home
            '''
        }
    
        stage('清理环境') {
            sh '''
                ## 删除原有war包
                rm -f $tomcat_home/webapps/ROOT.war
                rm -rf $tomcat_home/webapps/ROOT
            '''
        }
    
        stage('部署新的war包') {
            sh '''
                cp jeesite/web/target/web.war $tomcat_home/webapps/
                cd $tomcat_home/webapps
                mv web.war ROOT.war
            '''
        }
    
        stage('启动tomcat') {
            sh '''
                JENKINS_NODE_COOKIE=dontkillme
                cd $tomcat_home/bin
                sh startup.sh
            '''
        }
    }
    

    3.2在Jenkins中添加环境变量

    为了提高灵活性,我们将MySQL等相关配置,添加到Jenkins任务的环境变量中,供pipeline脚本调用。

    在节点属性中分别添加以下环境变量:mysql_ip(mysql的ip),mysql_port(mysql的端口),mysql_user(mysql的用户名),mysql_pwd(密码),tomcat_home(tomcat的根路径)环境变量

    在这里插入图片描述

    3.3 运行任务

    在这里插入图片描述

    登录查看是否项目发布成功

    在这里插入图片描述

    三.docker部署持续交付实战

    1.任务介绍

    在这里插入图片描述
    在这里插入图片描述

    2.Jenkins集成docker持续交互实战

    实战流程如下:拉取源码》编译war包》使用war包构建新的docker镜像》启动docker镜像

    (1)dockerfile编写

    我们使用dockerfile文件来生成新的的镜像

    在这里插入图片描述

    # 指定基础镜像
    FROM tomcat:latest
    
    # 指定维护者信息
    LABEL maintainer="hahaha"
    
    # 设置容器启动时的环境变量
    ENV NGINX_VERSION=1.17.9
    
    # 设置构建镜像时的环境变量
    ARG WORK_PWD=/usr/local/tomcat
    
    # 设置容器用户
    USER root
    
    # 在初始的tomcat容器中,webapps是一个空文件夹。而webapp相关文件都在webapps.dist文件夹下。所以我们执行下面的命令,将webapps.dist改名为webapps
    RUN rm -rf webapps && mv webapps.dist/ webapps && cd webapps && rm -rf ROOT
    
    # 设置工作目录
    WORKDIR $WORK_PWD/webapps
    
    # 复制代码文件war包到webapps目录下,并改名为ROOT.war
    COPY ./web.war ./ROOT.war
    
    # 设置容器的默认端口
    EXPOSE 8080
    
    
    # 设置停止容器的策略
    STOPSIGNAL SIGRTMAX
    

    (2)编写pipeline文件

    pipeline {
    	# 指定运行任务的节点
        agent {
            label 'salve-mylinux2'
        }
    
        environment {
            docker_image_name = 'jeesite4' # 设置docker镜像名称为变量
            docker_container_name = 'iJeesite4' # 设置docker容器名称为变量
        }
    
    
        stages{
            stage('同步源码') {
                steps{
                    sh """
                        mkdir jeesite || echo "the dir jeesite is exit"
                    """
                    dir("jeesite"){
                        git credentialsId:'gitee_ouyi', branch:'master', url:'git@gitee.com:xxx/jeesite4.git' # 项目源码地址
                    }
                }
            }
    
            stage('设定配置文件'){
                steps{
                    sh '''
                        . ~/.bash_profile
                        cd jeesite/web/src/main/resources/config
                        sed -i "s/mysql_ip/${mysql_docker_ip}/g" application.yml
                        sed -i "s/mysql_port/${mysql_port}/g" application.yml
                        sed -i "s/mysql_user/${mysql_user}/g" application.yml
                        sed -i "s/mysql_pwd/${mysql_pwd}/g" application.yml
                    '''
                }
            }
    
            stage('Maven 编译'){
                steps {
                    sh '''
                        . ~/.bash_profile
    
                        cd jeesite/root
                        mvn clean install -Dmaven.test.skip=true
    
                        cd ../../jeesite/web
                        mvn clean package spring-boot:repackage -Dmaven.test.skip=true -U
                    '''
                }
            }
    
            stage('停止 / 删除 现有Docker Container/Image '){
                steps {
                    script{
                        try{
                            sh 'docker stop $docker_container_name'
                        }catch(exc){
                            echo 'The container $docker_container_name does not exist'
                        }
    
                        try{
                            sh 'docker rm $docker_container_name'
                        }catch(exc){
                            echo 'The container $docker_container_name does not exist'
                        }
    
                        try{
                            sh 'docker rmi $docker_image_name'
                        }catch(exc){
                            echo 'The docker image $docker_image_name does not exist'
                        }
                    }
                }
            }
    
            stage('生成新的Docker Image'){
                steps {
                    sh '''
                        cd jeesite/web/bin/docker
                        rm -f web.war
                        cp ../../../web/target/web.war .
                        docker build -t $docker_image_name .
                    '''
                }
            }
    
            stage('启动新Docker实例'){
                steps {
                    sh '''
                        docker run -d --name=$docker_container_name -p 8981:8080 $docker_image_name
                    '''
                }
            }
        }
    }
    

    (3)Jenkins中创建任务

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    展开全文
  • 除此之外,还可用于数据挖掘、监测、自动化测试、信息处理和历史片段(历史记录)打包等了解完Scrapy框架后,我们就来看看怎么安装和使用吧安装方法有两种:1.如果你电脑上有Anaconda的话,可以使用这种方法windows+r>>>...
  • 根据官方文档的解释,tox是一个管理测试虚拟环境的命令行工具,可以支持穿件隔离的python环境,在里面可以安装不同版本的python解释器和项目的各种依赖库,可以进行自动化测试、打包以及持续集成。 tox能做什么 ...
  • ATP自动化测试平台(C#/Java)◇ SoapUI接口自动化测试框架(Groovy/华为)◇ 持续集成自动化打包框架(Java/华为)◇ 区块链性能测试框架(Python)等。擅长框架/平台设计开发、团队管理、团队技能提升培训,技术瓶颈...
  • 自动目标 自动生成,优化和人工学习 AutoGOAL是一个Python库,用于自动找到解决给定任务的最佳方法。 它主要是为自动机器学习(aka )设计的,但是它可以...AutoGOAL首先是自动化机器学习的框架。 因此,它预先打包
  • 根据官方文档的解释,tox是一个管理测试虚拟环境的命令行工具,可以支持穿件隔离的python环境,在里面可以安装不同版本的python解释器和项目的各种依赖库,可以进行自动化测试、打包以及持续集成。 tox能做什么 –...
  • 根据官方文档的解释,tox是一个管理测试虚拟环境的命令行工具,可以支持穿件隔离的python环境,在里面可以安装不同版本的python解释器和项目的各种依赖库,可以进行自动化测试、打包以及持续集成。 tox能做什么 ...
  • pytest接口自动化测试框架+项目实例

    万次阅读 多人点赞 2019-04-10 20:41:42
    一、基础框架:测试用例;测试数据;测试报告------------实现逻辑和数据分离,后期可以增加日志、公用配置、封装完善 1、 项目背景:http 接口、pycharm、pytest/unitest、python 先实现一个接口执行,引入 ...
  • Python 爬虫框架 Scrapyd 集群管理的全功能 web UI,支持 Scrapy 日志分析和可视自动打包、定时器任务和邮件通知等特色功能
  • 到现在把python selenium 大部分使用的函数API都熟悉了一遍,算是可以自己写一些脚本了,入门自动化了,后面准备研究些框架问题,同时加强练习。也秉承先手动敲代码的习惯,发现很多拼写错误。听大牛说从手工到学会...
  • Django 是一个用 Python 编写的 Web 应用程序框架,遵循 MVC(模型-视图-控制器)架构。它是免费的,并在开源许可下发布。它速度很快,旨在帮助开发人员尽快将他们的应用程序上线。 在本教程中,我将逐步向你展示在 ...
  • ATP自动化测试平台(C#/Java) ◇ SoapUI接口自动化测试框架(Groovy/华为) ◇ 持续集成自动化打包框架(Java/华为) ◇ 区块链性能测试框架(Python)等。擅长框架/平台设计开发、团队管理、团队技能提升培训,技术...
  • 对输入的域名或IP进行自动化信息搜集与扩展扫描,支持添加POC进行突破检测,扫描结果可视化显示在Web界面上 使用python3编写,多线程+多进程进行资产扫描,前端使用html + css + javascript进行拆分扫描系统的可视化...
  • Django 是一个用 Python 编写的 Web 应用程序框架,遵循 MVC(模型-视图-控制器)架构。它是免费的,并在开源许可下发布。它速度很快,旨在帮助开发人员尽快将他们的应用程序上线。 在本教程中...
  • 微信自动添加好友软件打包下载

    千次阅读 2018-09-10 23:46:59
    因为需要添加大量的微信好友,所以专门编写了一个脚本来执行。解决了非常大的问题,让想要添加...测试行业的人都会或多或少的接触到这个框架,这个框架广泛运用在app自动化测试。 具体实现方式,通过脚本调用appi...
  • The Modular toolkit for Data Processing (MDP) ,用于数据处理的模块工具包,一个Python数据处理框架。 从用户的观点,MDP是能够被整合到数据处理序列和更复杂的前馈网络结构的一批监督学习和非监督学习算法和...
  • 1,自动化核心框架 2,自动化测试的好处 3,自动化的前提 4,自动化测试的场景 5,元素定位的8种方式 6,如果一个元素无法定位,一般会考虑哪些原因 7,driver.close()和driver.quit()的区别 8,自动化脚本断言 9,...
  • awesome-python 是 vinta 发起维护的 Python 资源列表,内容包括:Web 框架、网络爬虫、网络内容提取、模板引擎、数据库、数据可视、图片处理、文本处理、自然语言处理、机器学习、日志、代码分析等。由「开源前哨...
  • python3 scrapy安装教程

    2020-09-11 17:35:18
    Scrapy用途非常广泛,主要用于抓取特定web站点的信息并从中提取特定结构的数据,除此之外,还可用于数据挖掘、监测、自动化测试、信息处理和历史片段(历史记录)打包等 了解完Scrapy框架后,我们就来看看怎么安装和使用吧...
  • ├─4.13、python自动化运维+web监控系统 │ ├─4.14、使用pyqt开发windows gui程序及打包 │ ├─4.15、Javascript 进阶 │ ├─4.16、MemCache │ ├─4.1、Photoshop切片 │ ├─4.2、...
  • Scray框架工作原理

    2019-07-07 19:12:00
    Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。 Scrapy是一个为遍历爬行网站、分解获取数据而设计的应用程序框架,它可以应用在广泛领域:数据挖掘、信息处理和或者历史片(历史记录)打包等等 ...
  • server上安装Django.Django是一种能快速且尽可能自动化开发Python web程序的web框架.我讲在这篇教程和Apache2,mod_Python一起使用它.这篇howto可以当作使用指南:它不包括理论背景,在网络 上很多文档中也被采用过. ...
  • 一个以web为载体的自动化测试框架,支持js所有语法和安卓 ios所有原生方法的调用. 并可以通过tcp udp websocket等网络协议提供接口支持, 开发者可以直接h5+js编写自动化程序 生成apk. 也可以通过接口用其它语言 如易...
  • 十一、最新尚硅谷自动化构建工具 视频 (1)webpack 链接: 密码: ru45 (2)Gulp 链接: 密码: nprj (3)Grunt 链接: 密码: m7kq 十二、尚硅谷React视频 密码:xz7c 十三、尚硅谷CSS2.1 视频 密码:jybl 十三...
  • SnapperML是结合了现有技术和得到良好支持的技术的用于实验跟踪和机器学习操作框架。 这些技术包括Docker, 和等。 该框架提供了一个自以为是的工作流程,可以在本地环境或云上设计和执行实验。 ml实验包括: ...
  • ,但是有时候,我们在测试过程中,当自动化任务很多,需要部署很多的机器去跑工程时,我们又不希望去频繁的部署安装Python环境,所以大部分公司都会将测试框架及测试用例打包为exe文件去执行。 今天我在打包的过程...
  • 一款利用加载器以及Python反序列绕过AV的在线免杀工具 因为打包方式的局限性,不能跨平台,若要生成exe格式的只能在Windows下运行本项目 打包速度有点慢,提交后稍等一会 开发环境及运行 前端使用框架,后端使用...

空空如也

空空如也

1 2 3 4
收藏数 77
精华内容 30
关键字:

python自动化框架打包

python 订阅