软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)

目录

1.1 常见的app类型:
1.2 为什么选择Appium

1.3 Appium优点
1.4 Appium的设计
(二)appium client及server参数
2.1 Appium client
2.2 Appium server
2.2.1 原理
2.2.2 Server -args 启动参数

(三) Find_element元素定位
3.1 element的定位方法:
3.2 Selenium 定位方式:
3.3 Appium 定位方式:
3.4 by_id('id')定位:
3.5 by_xpath('xpath')定位方式:
3.6 by_class_name定位:
3.7 By_android_uiautomator定位:
(1)UiSelector
(2)UIScrollable
3.8 By_accessibility_id定位



(四)Appium自动化测试框架介绍与搭建
4.1 框架的几大要素
4.2 框架的分层思想
4.3 如何持续集成
4.4 环境搭建
4.5 Nose框架介绍


4.6 框架数据的管理:


(五)框架中的AppiumServer模块设计与编写
(六)框架内自动化测试用例设计和方法封装

(七)Jenkins持续集成
7.1 需要安装的插件

7.2 SMTP邮件服务配置

7.3 app自动化测试工程
(一)Appium简介
1.1 常见的app类型:

  • native app--- 纯原生app
  • Hybrid app--- 原生app中内嵌了webview
  • Web app --- html5的应用


1.2 为什么选择Appium
借网上一张图:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

1.3 Appium优点
  • 开源
  • 跨架构:NativeApp、Hybird App、Web App
  • 跨设备:Android、iOS、Firefox OS
  • 不依赖源码
  • 使用任何WebDriver兼容的语言来编写测试用例。比如Java,Objective-C,JavaScript with Node.js,PHP,Python,Ruby,C#,Clojure或者Perl.
  • 不需要重新编译APP


1.4 Appium的设计 Appium的真正的工作引擎是第三方自动化框架,支持:
  • 苹果的UI Automator框架,支持ios平台app自动化;
  • google’s uiautomator 只支持android4.2以上,仅支持NativeApp原生控件;
  • Selendrid基于Instrumentation框架,可测试android2.3以上的系统,支持Hybrid、NativeApp;
  • Chromedriver 支持webapp、hybrid App;
Appium把这些第三方框架封装成一套API,即WebDriver API
Appium扩充了WebDriver的协议,在原有的基础上添加自动化相关的API方法。


(二)appium client及server参数
2.1 Appium client Appium架构图,Client即webdriver script部分(脚本部分)
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

Appium运行时候Server端会监听 Client端发送的命令,接着在移动设备上执行这些命令,然后将执行结果放在HTTP响应中返还给客户端。

2.2 Appium server
2.2.1 原理
Appium-Server端是基于node.js开发的程序,是http服务器,专门接收从client发送过来的命令,然后把命令发送到bootstrap.jar的客户端(bootstrap是server端的一部分),bootstrap按照命令驱动UIautomator来实现命令的操作,完成后把结果返回到server,server再通知appium-client。

启动appium server方式:
(1)通过源码的方式启动,即:到源码的目录下,执行Node
(2)在命令行输入appium来启动
(3)使用appium.exe启动
个人推荐使用appium命令启动,便于进行CI集成(jenkins)

2.2.2 Server -args 启动参数
Server启动时的设定:
  • 启动server时指定设备
  • 启动server时指定端口号
  • 设定session启动的一些操作等等

appium启动命令:
appium -p port --full-reset

常用的args:
(1)指定设备
-U,-udid: 指定连接的物理设备的UDID(通过adb devices 查看连接了那些设备)
(2)指定apk的路径
--app: 指定apk文件的绝对路径
(3)指定日志的输出
-g,--log: 将日志输出到指定的文件
(4)session间状态管理
--full-reset: session结束之后会卸载应用
--no-reset: session之间不会操作应用
(5)指定监听的ip和端口
-a,--address: 指定监听的端口
-p,--port: 指定server的端口,默认是4723
-bp,--bootstrap-port: 指定连接设备的端口号,默认是4724
--selendroid-port: 指定与selendroid交互的端口,默认是8080
--chromedriver: 指定chromedriver运行的端口,默认是9515

(三) Find_element元素定位
3.1 element的定位方法: (1)find_element_by_""
通过定位元素的属性得到一个元素对象,若无返回则抛异常
(2)find_elements_by_""(多个元素)
通过定位元素的属性得到一个包含一个或多个元素列表(数组),无返回则抛异常

3.2 Selenium 定位方式: (1)find_element_by_id('id') 'id'是唯一的
(2)Find_element_by_xpath(‘xpath’)‘xpath’可以是属性,也可以使标准的路径
(3)Find_element_by_link_text(‘login’)‘login’是a标签里面的文字login
(4)Find_element_by_name('name')input标签里会用到
(5)Find_element_by_class_name('className')用法类似id,不一定唯一
(6)Find_element_by_css_selector('css’)webUI特有的定位方式

3.3 Appium 定位方式: (1)Find_element_by_android('uiautomatorcode')使用UIautomator的代码直接进行定位,UIautomator code是指所有的UIautomator定位方式(比如new UiSelector或者new UiScrollable做定位)
(2)Find_element_by_accessibility_id('text'or'id'or'content-desc')
(3)Find_element_by_ios('uiautomationcode')用法同等(1)

3.4 by_id('id')定位: (1)resourceId
@appium1.4.0以前的版本
clock=driver.find_element_by_id('com.android.deskclock:id/analog_appwidget')

@Appium1.4.0或以后的版本
clock=driver.find_element_by_id('analog_appwidget')

Resourceid是相对唯一的,比如定义list(listid)列表下有多个item(itemid),此时id就会有重复,需要用find_elements

(2)Content-desc
例如:clock=driver.find_element_by_id('02:51')
优先匹配resourceId,然后匹配content-desc

3.5 by_xpath('xpath')定位方式: (1)完整的路径
NewsText=driver.find_element_by_xpath("//FrameLayout[1]/LinearLayout[1]/.......")

(2)通过元素属性指定

//uiautomator模式 NewsText=driver.find_element_by_xpath("//android.widget.TextView[contains(@text,"新闻")]") //selendroid模式 Driver.find_element_by_xpath("TextView[contains(@value,"新闻")]") NewsText.click()

3.6 by_class_name定位: className即android中的class,适用于控件比较少的情况下
Find_element_by_class_name('android.widget.TextView')[1]//获取当前页面所有的textview,然后按顺序获取

3.7 By_android_uiautomator定位: (1)UiSelector
Find_element_by_android_uiautomator(‘newUiSelector().resourceId(“com.sankuai.meituan:id/city_button”)’)


(2)UIScrollable
Find_element_by_android_uiautomator(‘new UiScrollable(newUiSelector().scrollable(true)).scrollintoView(new UiSelector().text(“新闻”))’)

3.8 By_accessibility_id定位 (1)resourceId
(2)Content-desc
(3)Text
Find_element_by_accessibility_id(u'美食')
优先匹配content-desc,然后匹配resourceId,最后匹配text
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片


(四)Appium自动化测试框架介绍与搭建
4.1 框架的几大要素 自动化通用要素(web/接口/app):
  • 数据/环境管理 ---数据如何维护(测试环境/线上环境账号、数据的不同、维护),环境切换(测试环境/线上环境/预发布环境)
  • Log日志 ---appium server的日志,脚本代码print的日志、框架运行的日志、手机运行时的日志
  • 通用方法(util)---在数据、环境的处理上,对数据库的操作、文件的读写等较常用的方法
  • 框架运行机制 ---框架是按照什么样的顺序运行,appium server何时启动、何时得到设备、用例testcase套件怎么跑等
  • Result报表 ---测试用例运行完毕后,查看运行的信息、错误的失败的情况进行报告解析

android自动化特有的驱动要素:
  • Appium server管理 ---如果有很多的设备、很多的用例,通过管理不同的appium server端口、appium server对应的设备号、appium server不同的启动状态
  • Driver管理 ---即session,每一个session可能对应不同的desired capibilities,对应不同的场景,是否每个用例脚本中要见session都需要考虑,对测试效率有影响
  • 元素对象管理 ---比如pageobject设计模式
  • Android设备管理 ---结合appiumserver管理使用,关注设备是否空闲,比如把100个用例分发到不同设备跑,提高效率,对android兼容性测试也有帮助
4.2 框架的分层思想 开发者角度:
测试套件(测试框架junit/testNG/unitest)---本次使用nose的测试框架(基于unitest),解决数据运行的管理、解决框架的运行机制、result报表(java推荐testNG,python推荐用nose)
Server ---包括设备和appium管理,才能对并发测试进行很好分配(需要了解多线程和线程锁等)

脚本编写角度:
脚本(testcase)---只写testcase,不要写数据
数据(data)
元素(Element)
三个层面不能掺杂,脚本调用数据,使用时,只需要把数据填充到一个地方,脚本调用就ok,数据变动不会影响到脚本

4.3 如何持续集成 什么是持续集成
CI平台(Continuous Integration)
包含要素:
统一的代码平台
自动触发构建、完成测试得出报告
提交代码会触发构建
持续集成应该是一个完整的方案
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

实战内容:
  • Python版本:
基于python+Nose+Nose-testconfig(管理数据配置)+Logging(日志系统)+Appium +jenkins+SVN(Git)
  • Java版本:
Java+TestNG+ReportNG+Maven+Appium+SVN(Git)

4.4 环境搭建
  • Java环境:jdk1.8.0_151
  • Android开发环境:adt-bundle-windows-x86_64-20140702
  • Python环境:python2.7.9
  • Appium环境:nodejs v5.6.0,appium1.3.4
  • 脚本开发IDE:pycharm
  • 其他组件:Nose&&Nose-testconfig,selenium,Appium-python-client,nosehtmloutput-2,nose-html-reporting插件等
环境搭建网上都有很多资料,此处不再赘述。

4.5 Nose框架介绍 (1)继承自unitest,比unitest更加简单,功能更强大
(2)Nose提供了递归查找测试套件的功能,而unitest是代码中通过调用unitest库的testrun的方法去执行当前模块下的测试用例(代码层面控制不方便),nose中只需要通过nose命令就可以递归的寻找python文件,通过正则匹配的方式发现test开头或者包含test的文件、方法,执行测试。
Nosetest支持较多方便实用的插件,比如html报告的生成等集成插件
(3)支持setup、teardown函数
有四种作用域:
  • package层
  • Module层
  • Class层
  • Function层
(4)可生成xml报告或html报告
例子如下:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

_init_.py文件
def setUp(): print 'package setUp' def tearDown(): print 'package tearDown'


TestMyCase.py文件





4.6 框架数据的管理: 全局数据(影响到框架运行流程的数据)-----通过testconfig文件来实现支持
(1)存放通用性的全局数据,常见的如域名地址,接口测试时的测试/线上环境等域名是不同的;
(2)对于appium来说,可存放端口号(比如appium启动端口4723,如果并发测试,启动多个server需要管理多个端口号)和一些全局控制参数(封装好的点击的方法,错误时截图)。

局部数据(不会影响到框架运行的全局数据) ---通过html或excel表格来获取
比如发文章、评论的时用的数据

私有数据 ---写到代码模块特定用例会用到的数据

Nose-testconfig的使用(全局数据的一个插件)
①在.cfg文件中声明testconfig文件目录;
[nosetests] ; --with-unit output xml report with-html-output=True html-out-file=result1.html tests=testcase/testLogin/testLogin.py nocapture=True verbose=Truetc-format=python tc-file=conf/env/testconfig.py; exclude=testcase/testMyCase.py


②定义config字典,字典中可以填入全局变量
global config config = {}config['packageName'] = 'com.hwd.test' config['all']= 'acj'config['appiumPort'] = 4723 config['selendroidPort'] = 8120 config['bootstrapPort'] = 4750 config['chromiumPort'] = 9553config['app'] = r'D:\PyCharmWorkSpace\DemoProject\src\testData\xxxx-release-v4.2.0-2018-03-07.apk' config['appPackage'] = 'com.cashregisters.cn' config['appActivity'] ='com.hkrt.qpos.presentation.screen.welcome.WelcomeActivity'config['appWaitActivity'] = ''


③在自动化脚本中导入模块,并使用变量
#coding:utf-8 from appium import webdriver from testconfig import config import logging,timeclass testLogin: def __init__(self): self.logger =logging.getLogger(__name__)def setUp(self): desired_caps = {} desired_caps['platformName'] = 'android' desired_caps['platformVersion'] = '6.0.0' desired_caps['deviceName'] = 'TWGDU1700002279' desired_caps['app'] = config['app'] desired_caps['appPackage'] = config['appPackage'] desired_caps['appActivity'] = config['appActivity'] desired_caps['noReset'] = 'true' desired_caps['unicodeKeyboard'] = 'true' desired_caps['resetKeyboard'] = 'true' if config['appWaitActivity'] != None: desired_caps['appWaitActivity'] = config['appWaitActivity']self.logger.info('Session Starting...')self.driver =webdriver.Remote('http://localhost:4723/wd/hub',desired_caps) def tearDown(self): self.logger.info('Quit!') self.driver.quit()def testLogin(self): self.logger.info('test logining!!!') self.driver.implicitly_wait(10) userFiled =self.driver.find_element_by_id('com.cashregisters.cn:id/phone_id') pwdFiled =self.driver.find_element_by_id('com.cashregisters.cn:id/password_id') userFiled.send_keys('18610000920') pwdFiled.send_keys('1234qwer') self.driver.find_element_by_id('com.cashregisters.cn:id/phone_id').click() self.driver.find_element_by_id('com.cashregisters.cn:id/loginbutton').click() time.sleep(2)


软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片


(五)框架中的AppiumServer模块设计与编写
Appium Server的启动方式
(1)使用appium.exe(windows)/appium.app(mac)
(2)通过代码控制命令行启动,通过下面例子可以看明白

例子:
①appium.py文件编写appium的启动模块

import logging,os,time import subprocess from testconfig import configclass appium(object):def __init__(self): self.logger = logging.getLogger(__name__)def start(self): self.logger.info('Starting Appium Server ...') currentTime = time.strftime('%Y%m%d%H%M%S',time.localtime())udid = self.getUdid() appiumPort = config['appiumPort'] bootStrapPort = config['bootstrapPort'] selendroidPort = config['selendroidPort'] chromiumPort = config['chromePort'] logPath = os.path.abspath(os.path.join(os.getcwd(),'log','AS'+currentTime+'.log')) try: self.process = subprocess.Popen('appium --port={} --bootstrap-port={} --selendroid-port={} --chromedriver-port={}' '--log={} -U{}'.format(appiumPort,bootStrapPort,selendroidPort,chromiumPort,logPath,udid), stdout=subprocess.PIPE, shell=True) except Exception,e: self.logger.error('start appium server failed!') self.logger.error('errorMsg:{}'.format(e)) time.sleep(5)def stop(self): self.logger.info('stop appium server.') self.process.kill() os.system('taskkill /im node.exe /f')def getUdid(self): 'adb devices' cmd = 'adb devices' output = subprocess.Popen(cmd,stdout=subprocess.PIPE,shell=True) infoList = output.stdout.read().strip('List of devices attached').split() devicesList = [] if infoList != 0: for deviceinfo in infoList: if deviceinfo != 'devices': devicesList.append(deviceinfo) return devicesList[0]


②testconfig.py文件设置启动端口

global config config = {}config['packageName'] = 'com.hwd.test' config['all']= 'acj'config['port'] = 4723 config['selendroidPort'] = 8120 config['bootstrapPort'] = 4750 config['chromePort'] = 9553


③用例中初始化启动文件__init__.py
#coding:utf-8 import logging from src.AppiumServerimport appiumserverlogger = logging.getLogger(__name__)def setUp(self): logger.info(u'启动Appium Server') appiumserver().start()def tearDown(self): logger.info(u'关闭Appium Server') appiumserver().stop()


④编写测试用例
#coding:utf-8 from appium import webdriver from testconfig import config import logging,timeclass testLogin: def __init__(self): self.logger =logging.getLogger(__name__)def setUp(self): desired_caps = {} desired_caps['platformName'] = 'android' desired_caps['platformVersion'] = '6.0.0' desired_caps['deviceName'] = 'TWGDU17000002279' desired_caps['app'] = config['app'] desired_caps['appPackage'] = config['appPackage'] desired_caps['appActivity'] = config['appActivity'] desired_caps['noReset'] = 'true' desired_caps['unicodeKeyboard'] = 'true' desired_caps['resetKeyboard'] = 'true' if config['appWaitActivity'] != None: desired_caps['appWaitActivity'] = config['appWaitActivity']self.logger.info('Session Starting...')self.driver =webdriver.Remote('http://localhost:4723/wd/hub',desired_caps) def tearDown(self): self.logger.info('Quit!') self.driver.quit()def testLogin(self): self.logger.info('test logining!!!') self.driver.implicitly_wait(10) userFiled =self.driver.find_element_by_id('com.cashregisters.cn:id/phone_id') pwdFiled =self.driver.find_element_by_id('com.cashregisters.cn:id/password_id') userFiled.send_keys('18610000920') pwdFiled.send_keys('1234qwer') self.driver.find_element_by_id('com.cashregisters.cn:id/phone_id').click() self.driver.find_element_by_id('com.cashregisters.cn:id/loginbutton').click() time.sleep(2)


执行顺序:
框架的入口src/__init__.py (初始化日志相关的东西)—>testcase/__init__.py文件(package层的init)—>testLogin/__init__.py启动appiumserver—>testLogin方法


(六)框架内自动化测试用例设计和方法封装
(1)创建appiumserver,上面已介绍
(2)在run_test.cfg中指定只运行自动化测试脚本testLoginSuccess.py文件

[nosetests] ; --with-unit output xml report with-xunit=True tests=testcase/testLogin/testLoginSuccess.py nocapture=True verbose=Truetc-format=python tc-file=conf/env/testconfig.py; exclude=testcase/testMyCase.py


(3)新建BaseTestCase基类(测试用例中继承BaseTestCase即可使用)

#coding:utf-8 from appium import webdriver from testconfig import config import loggingclass BaseTestCase: def __init__(self): self.logger = logging.getLogger(__name__)def setUp(self): desired_caps = {} desired_caps['platformName'] = 'android' desired_caps['platformVersion'] = '6.0.0' desired_caps['deviceName'] = 'Google Nexus 5' desired_caps['app'] = config['app'] desired_caps['appPackage'] = config['appPackage'] desired_caps['appActivity'] = config['appActivity'] desired_caps['noReset'] = 'true' desired_caps['unicodeKeyboard'] = 'true' desired_caps['resetKeyboard'] = 'true' if config['appWaitActivity'] != None: desired_caps['appWaitActivity'] = config['appWaitActivity'] self.logger.info('Session Starting...')self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) def tearDown(self): self.logger.info('Quit!') self.driver.quit()


(4)新建不同otherConfig文件,指定不同的端口、实现不同apk应用运行不同的用例,上面已介绍

global config config = {}config['packageName'] = 'com.hwd.test' config['all']= 'acj'config['appiumPort'] = 4724 config['selendroidPort'] = 8124 config['bootstrapPort'] = 4754 config['chromiumPort'] = 9554config['app'] = r'D:\PyCharmWorkSpace\DemoProject\src\testData\xxx-release-v4.2.0-2018-03-07.apk' config['appPackage'] = 'com.cashregisters.cn' config['appActivity'] = 'com.hkrt.qpos.presentation.screen.welcome.WelcomeActivity'config['appWaitActivity'] = None


(5)测试脚本中导入使用

import logging,time from src.util.commonBase import* from src.testcase.baseTestCase import BaseTestCaseclass testLoginSucess(BaseTestCase):def __init__(self): BaseTestCase.__init__(self) self.logger = logging.getLogger(__name__)def setUp(self): BaseTestCase.setUp(self)def tearDown(self): BaseTestCase.tearDown(self)def testLoginFailed(self): self.logger.info('test begaining!') self.driver.implicitly_wait(10) inputById(self.driver,'com.cashregisters.cn:id/phone_id','111111') inputById(self.driver, 'com.cashregisters.cn:id/password_id', 'aaaaaa') clickElementById(self.driver,'com.cashregisters.cn:id/loginbutton') time.sleep(2)def testLoginSuccess(self): self.logger.info('test case 2.') self.driver.implicitly_wait(10) userFiled = self.driver.find_element_by_id('com.cashregisters.cn:id/phone_id') pwdFiled = self.driver.find_element_by_id('com.cashregisters.cn:id/password_id') userFiled.send_keys(18610000920) pwdFiled.send_keys(1234qwer) self.driver.find_element_by_id('com.cashregisters.cn:id/loginbutton').click() time.sleep(2) self.driver.find_element_by_id('android:id/button2').click()


(6)封装方法 ---比如封装登录按钮、截图等等,使用例看起来更简洁

import logging logger = logging.getLogger(__name__) def clickElementById(driver,elementId): ''' :click element by id ''' logging.info('click element by Id:{}'.format(elementId)) try: driver.find_element_by_id(elementId).click() except Exception,e: logger.error('Click Id:{} Fail,error Msg:{}'.format(elementId,e))def inputById(driver,elementId,text): ''' :input sth into element by id ''' logger.info('input {} into element by id:{}'.format(text,elementId)) try: driver.find_element_by_id(elementId).send_keys(text) except Exception,e: logger.error('input fail,error msg:{}'.format(e))


(7)测试用例中可以这样使用:

inputById(self.driver,'com.cashregisters.cn:id/phone_id','111111') inputById(self.driver, 'com.cashregisters.cn:id/password_id', 'aaaaaa') clickElementById(self.driver,'com.cashregisters.cn:id/loginbutton')

(8)测试报告
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片


(七)Jenkins持续集成 Jenkins配置:网上教程很多,就不赘述,挑一些项目相关的实例

7.1 需要安装的插件 此处用到2个插件:
(1)邮件插件email extension
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

(2)报告插件html-reporting和groovy
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

7.2 SMTP邮件服务配置 (1)管理员邮件配置:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

(2)SMTP服务器配置:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

(3)邮件通知配置:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

7.3 app自动化测试工程 (1)配置执行自动化测试的脚本:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片


(2)测试报告设置:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片


(3)收件人配置:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片


(4)测试报告模板:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片


(5)配置邮件什么情况下发送(成功发送/失败发送):
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片


(6)构建完后jenkins页面显示:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片


(7)最终邮件收到的报告:
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

好了 学习也就到此结束了 想了解更多相关知识请关注我吧!下面是小编想对读者大大们写的一封信哦! 记住要认真读哦!
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接免费拿走:
① 2000多本软件测试电子书(主流和经典的书籍应该都有了)
② 软件测试/自动化测试标准库资料(最全中文版)
③ 项目源码(四五十个有趣且经典的练手项目及源码)
④ Python编程语言、API接口自动化测试、web自动化测试、App自动化测试(适合小白
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

⑤ Python学习路线图(告别不入流的学习)
上图的资料 在我的QQ技术交流群里(技术交流和资源共享,广告进来腿给你打断)
可以自助拿走,群号768747503备注(csdn999)群里的免费资料都是笔者十多年测试生涯的精华。还有同行大神一起交流技术哦
————————————————
「学习资料 笔记 工具 文档领取」
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

扫描二维码,
备注“csdn999”
小姐姐邀你一起学习哦~~
和志同道合的测试小伙伴一起讨论测试技术吧!
软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

一定一定一定 要备注暗号:CSDN999
———————————————
【软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)】软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)
文章图片

    推荐阅读