一、认知ios自动化测试及框架:
通常,我们会选择那些业务稳定,需要频繁测试的部分来编写自动化测试脚本,其余的采用人工测试,人工测试仍然是iOS App开发中不可缺少的一部分。
测试种类
从是否接触源代码的角度来分类:测试分为黑盒和白盒(灰盒就是黑盒白盒结合,这里不做讨论)。
白盒测试的时候,测试人员是可以直接接触待测试App的源代码的。白盒测试更多的是单元测试,测试人员针对各个单元进行各种可能的输入分析,然后测试其输出。白盒测试的测试代码通常由iOS开发编写。
黑盒测试。黑盒测试的时候,测试人员不需要接触源代码。是从App层面对其行为以及UI的正确性进行验证,黑盒测试由iOS测试完成。
而iOS测试通常只有以下两个层次:
Unit,单元测试,保证每一个类能够正常工作
UI,UI测试,也叫做集成测试,从业务层的角度保证各个业务可以正常工作。
框架选择
啰里八嗦讲的这么多,自动化测试的效率怎么样,关键还是在测试框架上。那么,如何选择测试框架呢?框架可以分为两大类:XCode内置的和三方库。
选择框架的时候有几个方面要考虑:
测试代码编写的成本
是否可调式
框架的稳定性
测试报告(截图,代码覆盖率,…)
WebView的支持(很多App都用到了H5)
自定义控件的测试
是否需要源代码
能否需要连着电脑
是否支持CI(持续集成)…
我们首先来看看XCode内置的框架:XCTest。
XCTest又可以分为两部分:Unit Test 和 UI Test,分别对应单元测试和UI测试。有一些三方的测试库也是基于XCTest框架的,这个在后文会讲到。由于是Apple官方提供的,所以这个框架会不断完善。
二、框架选择:
按照iOS自动化测试框架的实现原理来划分,iOS自动化测试框架大致可以分为两个大类4种类型:
UI Automation系
扩展型UI Automation
驱动型UI Automation
非 UI Automation系
私有API型
注入编译型
1、UI Automation
UI Automation是Apple官方提供的UI自动化测试的解决方法,虽然直接使用非常不爽,但是还不得不说。虽然不好用,但是作为一些工具的底层实现还是需要了解的。官方教程 和 相关API文档 可以方便查阅
(1)扩展型UI Automation
TuneupJs是最早的iOS自动化测试工具,以JavaScript扩展库方法提供了很多好用js工具,最重要的是提供了超简洁的单元测试框架和持续继承解决方案。
ynm3k是笔者维护的iOS自动化测试框架,借鉴或者说抄袭了很多TuneUpJs的想法,在其基础上加入了UI控件定位的很多方法,让测试脚本更加简单便捷。当然之后还有后续的维护计划,过了变态的996以后会开始实施。
如果你偏爱这种类型的测试框架,我真心的推荐ynm3k。因为TuneUpJS已经不维护了,ynm3k还会做一些好玩的功能。最关键的是我觉得ynm3k的UI控件定位识别功能真的很棒。(老王卖瓜了这么长时间还请大家见谅)
(2)驱动型UI Automation
驱动型UI Automation 在自动化测试底层使用了UI Automation库,通过TCP通信的方式驱动UI Automation来完成自动化测试。通过这种方式,编辑脚本的语言不在局限于JavaScript,理论上讲可以是任何一种语言。所以有了iOSDriver和Appium.
iOSDriver和Appium还兼容了WebDriver的Json Wire Protocol协议,意味着你可以写WebDriver脚本来完成iOS自动化测试。当然在手机端的一些操作和Web端是不同的,iOSDriver和Appium都扩展了相关的功能。iOSDriver选择了多加入Java语言Jar包的方式支持,而Appium则选择了通过WebDriver注入JavaScript的方式支持。iOSDriver的实现方式决定了只能使用Java语言编辑脚本,而Appium则支持更多的语言。
当然让我真正选择Appium的原因是因为Appium更加轻便易用一些。如果你不经常玩Java,使用iOSDriver的时候一定会在环境设置方面卡很长的时间。
2、非 UI Automation系
(1)私有API型
直接使用私有API对UI界面进行操作是最简洁有效的自动化测试方式。私有API结合iOS单元测试框架OCUnit的组合完全非常棒,至少对iOS开发工程师来说。在这种类型的测试框架中,KIF是必须介绍的。原因很简单,Google在使用。对于那些对Google无理由崇拜的人们,赶紧用起来吧。
笔者关注了KIF一段时间,起初1.0版本的时候,真心的不好用。当然工具的维护者也意识到了这个方面的问题,推出了KIF的2.0版本,更新了很多的API。在2.0版本时,还不错。如果想做纯UI界面操作的话,不推荐使用KIF。
(2)注入编译型
注入编译型是指在编译时注入一个Server到App内部,通过Server对外通信完成UI操作指令的执行。其中最著名的代表为Frank和Calabash. 它们还是BDD测试框架的杰出代表。维护大单位都是全球知名的敏捷咨询公司。喜欢cucumber和ruby的人可以考虑。
三、iOS自动化– 常用iOS命令:
iOS命令:
获取设备的的UDID
idevice_id –list # 显示当前所连接设备的 udid
instruments -s devices # 列出所有设备,包括真机、模拟器、mac
ideviceinfo 可以在返回的数据中找到 udid
idevice_id -l
安装某个app
ideviceinstaller -i apppath 安装apppath下的app
ideviceinstaller -u [udid] -i [xxx.ipa] # xxx.ipa 为应用在本地的路径
卸载应用
ideviceinstaller -u [udid] -U [bundleId]
查看设备已安装的应用
ideviceinstaller -u [udid] -l # 查看设备安装的第三方应用
ideviceinstaller -u [udid] -l -o list_user # 同上,查看设备安装的第三方应用
ideviceinstaller -u [udid] -l -o list_system # 查看设备安装的系统应用
ideviceinstaller -u [udid] -l -o list_all # 查看设备安装的所有应用
获取设备信息
ideviceinfo -u [udid] # 获取设备信息
ideviceinfo -u [udid] -k DeviceName # 获取设备名称 同命令 idevicename
idevicename # 同上
ideviceinfo -u [udid] -k ProductVersion # 获取设备版本 10.3.3
ideviceinfo -u [udid] -k ProductType # 获取设备类型 iPhone 8,1
ideviceinfo -u [udid] -k ProductName # 获取设备系统名称
其他系统文件信息
ideviceinfo # 获取设备所有信息
idevicesyslog # 获取设备日志
idevicecrashreport -e test # 获取设备 crashlog,test 是文件夹需新建
idevicediagnostics # 管理设备状态 – 重启、关机、睡眠等
ios-deploy 常用命令
ios-deploy -c # 查看当前链接的设备
ios-deploy –[xxx.app] # 安装APP
ios-deploy –id [udid] –uninstall_only –bundle_id [bundleId] # 卸载应用
ios-deploy –id [udid] –list_bundle_id # 查看所有应用
ios-deploy –id [udid] –exists –bundle_id # 查看应用是否安装
列举设备安装的应用:
ideviceinstaller -l则可以列出手机上所有的用户安装的app
运行某个app
idevicedebug run ‘APP_BUNDLE_ID’可以直接launch某个app,当然,这个app必须是你通过development证书build到手机上的才行。
获取手机的设备版本:
Ideviceinfo -k ProductVersion
获取手机的设备名:
ideviceinfo -k ProductType
截图:
idevicescreenshot
录像:
xrecord –quicktime –list
xrecord –quicktime –name=”iPhone” –out=”/Users/blah/video/iphone.mp4″ –force
手机关机:idevicediagnostics shutdown # shutdown device
重启手机:idevicediagnostics restart # restart device
休眠熄屏灭屏): idevicediagnostics sleep # 类似于断开adb . disconnects from host)
四、IOS自动化App测试——安装app指令
IOS自动化运行
1) 安装iOS测试包相关命令
① 安装iOS测试包
$ ios-deploy –id [设备udid] –bundle [ipa路径]
例:
ios-deploy –id 315214497a82c001d0cac7541ddfaac3288c05b2 –bundle /data/uitest/UmeAutomationTestAndroid/res/app/ios/UmetripFree.ipa
ios-deploy –id 2d262872589c8272c7eaddb89bd9da750f170952 –bundle /data/uitest/UmeAutomationTestAndroid/res/app/ios/UmetripFree.ipa
② 查看当前设备udid
$ idevice_id -l
③ 查看当前设备信息
$ instruments -s devices
五、搭建IOS自动化测试环境-Appium
一、安装Homebrew工具
1、简介
Homebrew官网 http://brew.sh/index_zh-cn.html
Homebrew是神马
linux系统有个让人蛋疼的通病,软件包依赖,好在当前主流的两大发行版本都自带了解决方案,Red ha有yum,Ubuntu有apt-get
神马,你用mac os,不好意Mac os木有类似的东东,泪奔中几经折腾总算找到了第三方支持:Homebrew,Homebrew简称brew,是Mac OSX上的软件包管理工具,能在Mac中方便的安装软件或者卸载软件,可以说Homebrew就是mac下的apt-get、yum神器
2、Homebrew安装
Homebrew的安装非常简单,打开终端复制、粘贴以下命令,回车,搞定请放心使用,原汁原味的官方安装方法搬运)
ruby -e “$curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”
ps:不知道为什么执行这个命令有时会返回400,估计可能被墙了,过几分钟重试下一般就ok了,有图有真相
3、Homebrew使用
Homebrew使用没啥好说的了,常用的
搜索软件:brew search 软件名,如brew search wget
安装软件:brew install 软件名,如brew install wget
卸载软件:brew remove 软件名,如brew remove wget
二、安装libimobiledevice开源包(IOS开发的另类神器)
1、简介
libimobiledevice又称libiphone,是一个开源包,可以让Linux支持连接iPhone/iPod Touch等iOS设备。由于苹果官方并不支持Linux系统,但是Linux上的高手绝对不能忍受因为要连接iOS设备就换用操作系统这个事儿。因此就有人逆向出iOS设备与Windows/Mac Host接口的通讯协议,最终成就了横跨三大桌面平台的非官方版本USB接口library。经常用Linux系统的人一定对libimobiledevice不陌生,但是许多Windows和Mac用户也许就不知道了。事实上,它同iTools一样,都是可以替代iTunes,进行iOS设备管理的工具。因为源码是开放的,可以自行编译,所以对很多开发者而言可以说更为实用
参考:http://www.jianshu.com/p/6423610d3293
2、安装
指令:brew install libimobiledevice –HEAD
三、安装carthage
1、简介
Carthage的目标是用最简单的方式来管理Cocoa第三方框架
参考https://www.cnblogs.com/wendingding/p/5959322.html
Carthage 是用来解决 xcode project 依赖的,大家可能知道 cocoapod,那你就把 Carthage 理解成和 cocoapod 一样的东西就可以了,可以通过 brew install carthage 安装。
2、安装
指令:brew install carthage
四、安装nodejs
1、简介
简单的说 Node.js 就是运行在服务端的 JavaScript。
Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。
Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。
2、安装
按照官方的地址https://nodejs.org/en/download/。 下载.pkg文件安装
五、安装cnpm
1、简介
npm(node package manager)是nodejs的包管理器,用于node插件管理(包括安装、卸载、管理依赖等)
2、安装
cnpm由于某种原因,直接用npm下载安装会有好多网络问题,安装淘宝的cnpm要比npm好用) https://npm.taobao.org/
指令:npm install -g cnpm –registry=https://registry.npm.taobao.org
出现权限问题:在指令前加sudo
公司内网非常慢,建议用手机流量,巨快= =
六、安装ios-deploy
1、简介
ios-deploy是一个使用命令行安装ios app到连接的设备的工具,原理是根据os x命令行工程调用系统底层函数,获取连接的设备、查询/安装/卸载app。类似的工具有Fruitstrap,ideviceinstaller、node-ios-device等
2、安装
指令:cnpm install -g ios-deploy
(公司内网同样不行T_T)
七、安装xcpretty
1、简介
用于对xcodebuild的输出进行格式化。并包含输出report功能。
2、安装
指令:cnpm install xcpretty
八、安装appium
1、安装appium1.6.3到发帖为止,最新版本是1.6.3,要其他版本的跟上版本号就行了)这一步骤若出现安装jDK弹出框忽略就好
cnpm install -g appium@1.6.3
2、检验:输入命令:appium
九、appium-doctor
会提示装下面的xcode comment line tools
如出现这些问题后面解决(ANDROID_HOME要用安桌sdk)
十、安装appium-xcuitest-driver依赖
1、进入WebDriverAgent安装目录,运行bootstrap
cd /usr/local/lib/node_modules/appium/node_modules/appium-xcuitest-driver/WebDriverAgent (如果WebDriverAgent 所在路径和此不同,请自行查找)
mkdir -p Resources/WebDriverAgent.bundle sh ./Scripts/bootstrap.sh
在运行sh ./Scripts/bootstrap.sh很可能会有因为咱们大中华局域网而网络连接失败。方法就是去App store下载了一个VPN代理软件,我下了一个评分最多且免费的,的确很好用。我就不说软件名称了。
再次运行sh ./Scripts/bootstrap.sh 无报错就OK了
安装Xcode及Xcode Command Line Tools
–安装Xcode-最好用app store直接下载———————————————————–
检测是否安装好Xcode
$ xcode-select -p
显示这样说明安装好
/Applications/Xcode.app/Contents/Developer
如果没有装好,安装方法:
Xcode不同版本可以在以下网页下载,需要登陆apple帐户:
https://developer.apple.com/downloads/
–安装Xcode Command Line Tools—
1.调出安装窗口
$ xcode-select –install
2.点击 Install 安装
Click “Install” to download and install Xcode Command Line Tools.
用Xcode打开WebDriverAgent,并且编译
编译WebDriverAgentLib
编译WebDriverAgentRunner
六、使用Appium进行iOS的真机自动化测试
安装类库
Homebrew
如果没有安装过Homebrew,先安装 [ homebrew ]
npm
如果没有安装npm,请移步 [ node.js和npm安装 ]
安装依赖库
brew install libimobiledevice --HEAD
sudo npm install -g ios-deploy --unsafe-perm=true
如果执行sudo npm install -g ios-deploy --unsafe-perm=true
报错,执行sudo xcode-select --switch/Applications/Xcode.app/Contents/Developer/
如果没有安装 libimobiledevice,会导致Appium无法连接到iOS的设备,所以必须要安装,如果要在iOS10+的系统上使用appium,则需要安装ios-deploy
appium-doctor 安装
npm install appium-doctor -g
安装后执行appium-doctor –ios指令,可以查看与iOS相关配置是否完整,下图是全部配置都成功,如果出现有一项不正确在执行一次就可以,或者直接跳过
appium-doctor –ios
更新Appium中的WebDriverAgent
到WebDriverAgent下载最新版本的WebDriverAgent
cd 进入下载后的WebDriverAgent文件
执行 ./Scripts/bootstrap.sh
直接用Xcode打开WebDriverAgent.xcodepro文件
配置WebDriverAgentLib和WebDriverAgentRunner的证书
连接并选择自己的iOS设备,然后按Cmd+U,或是点击Product->Test
运行成功时,在Xcode控制台应该可以打印出一个Ip地址和端口号
在网址上输入http://192.168.2.101:8100/status,如果网页显示了一些json格式的数据,说明运行成功。
进入到Appium中的WebDriverAgent目录,目录路径如下/Applications/Appium.app/Contents/Resources/app/node_modules/appium/node_modules/appium-xcuitest-driver/
将自己下载并编译后的WebDriverAgent替换Appium原有的WebDriverAgent
在Appium-Desktop下载传送门中下载最新版本的Appium-Desktop
运行Appium-Desktop
开启start server
点击start new session并且在Desired Capabilities 中输入相关的参数后点击Start Session
运行成功后,会弹出一个控制界面,在该界面中可以控制手机上正在运行的程序
利用Appium-Python-Client进行iOS的自动化测试
安装python
brew install python
下载python-client
git clone https://github.com/appium/python-client.git
cd python-client
python setup.py install
在git上下载测试文件appiumSimpleDemo
开始自动化测试
打开下载后的appiumSimpleDemo文件,打开appiumSimpleDemo.xcodepro程序,配置下TARGET的签名
在appiumSimpleDemo的根目录执行编译指令,编译出一个app文件xcodebuild -sdk iphoneos -target appiumSimpleDemo -configuration Release
,编译成功后app文件的地址会打印在命令行中
配置python文件
打开appiumSimpleDemo中的appiumSimpleDemo.py文件,将,修改setup中的几个参数,将app的路径,设备的相关信息修改成当前连接设备的信息。
如果执行appiumSimpleDemo.py
报错File"/usr/local/Cellar/python@2/2.7.15/Frameworks/Python.framework/Versions
请升级python版本,如果你是iOS开发人员,请谨慎,升级python有可能是Xcode无法打包参考文稿
七、Mac端-Appium桌面版-iOS真机自动化测试
1. 启动appium,版本 1.13.0
2.点击红框里的,Edit Configurations。
3.配置一下ANDROID_HOME,JAVA_HOME
我的是这样的,如果你的没有下载,就先去下载(Android sdk)(JAVA):
ANDROID_HOME:/Users/yingying.zou/Library/Android/sdk
JAVA_HOME: /Library/Java/JavaVirtualMachines/jdk-12.0.1.jdk/Contents/Home
4.Start ServerRestart Now)
{
“platformName”: “ios”,
“deviceName”: “xxxiPhone”, (苹果手机设置-通用-关于本机,可查看名称)
“platformVersion”: “12.1.4”, (苹果手机设置-通用-关于本机,可查看版本)
“udid”: “xxxxxxxxxxxxxxxxxx”, (苹果手机连苹果电脑,iTunes里查看手机的摘要,能找到,右键复制,不会的话百度一下你就知道)
“bundleId”: “com.xxxxxxxxxxxx” 用xcode打开工程,能找到,或者你直接问iOS开发人员)
“xcodeOrgId”: “C2xxxxxxxx”, 十位字符的组织ID,是苹果开发证书的组织单位,appium可以通过十位组织单位ID找到相应的
组织,如果是连接真机测试APP的话,必须要设置组织参数。注意: 连接真机时,测试app的打包
签名证书必须要与xcodeOrgId里的一致,否则会报证书错误,错误代码是65。)
“xcodeSigningId”: “iPhone Developer” 固定的)
}
这个填好了可以Save As一下,下次直接打开就可以了。
5.Start Session,手机上会自动装一个webdriverapp的应用,然后才会启动要测试的app。(要测试的app提前在手机上存在着~)
到这一步了,随便点点逛逛,可以通过这个操控手机,看看日志了,有一点激动,虽然还有好多不明所以
下一步,要学习用python写测试脚本了~加了个油~
八、XCTest认识
本Markdown编辑器使用StackEdit修改而来,用它写博客,将会带来全新的体验哦:
Markdown和扩展Markdown简洁的语法
代码块高亮
图片链接和图片上传
LaTex数学公式
UML序列图和流程图
离线写博客
导入导出Markdown文件
丰富的快捷键
快捷键
加粗 Ctrl + B
斜体 Ctrl + I
引用 Ctrl + Q
插入链接 Ctrl + L
插入代码 Ctrl + K
插入图片 Ctrl + G
提升标题 Ctrl + H
有序列表 Ctrl + O
无序列表 Ctrl + U
横线 Ctrl + R
撤销 Ctrl + Z
重做 Ctrl + Y
Markdown及扩展
Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,然后转换成格式丰富的HTML页面。 —— [ 维基百科 ]
使用简单的符号标识不同的标题,将某些文字标记为粗体或者斜体,创建一个链接等,详细语法参考帮助?。
本编辑器支持 Markdown Extra , 扩展了很多好用的功能。具体请参考Github.
表格
Markdown Extra 表格语法:
可以使用冒号来定义对齐方式:
定义列表
Markdown Extra 定义列表语法:
项目1
项目2
定义 A
定义 B
项目3
定义 C
定义 D
定义D内容
代码块
代码块语法遵循标准markdown代码,例如:
脚注
生成一个脚注1.
目录
用 [TOC]来生成目录:
更多LaTex语法请参考 这儿.
UML 图:
可以渲染序列图:
或者流程图:
关于 序列图 语法,参考 这儿,
关于 流程图 语法,参考 这儿.
离线写博客
即使用户在没有网络的情况下,也可以通过本编辑器离线写博客(直接在曾经使用过的浏览器中输入write.blog.csdn.net/mdeditor即可。Markdown编辑器使用浏览器离线存储将内容保存在本地。
用户写博客的过程中,内容实时保存在浏览器缓存中,在用户关闭浏览器或者其它异常情况下,内容不会丢失。用户再次打开浏览器时,会显示上次用户正在编辑的没有发表的内容。
博客发表后,本地缓存将被删除。
用户可以选择 把正在写的博客保存到服务器草稿箱,即使换浏览器或者清除缓存,内容也不会丢失。
注意:虽然浏览器存储大部分时候都比较可靠,但为了您的数据安全,在联网后,请务必及时发表或者保存到服务器草稿箱。
浏览器兼容
目前,本编辑器对Chrome浏览器支持最为完整。建议大家使用较新版本的Chrome。
IE9以下不支持
IE9,10,11存在以下问题
不支持离线功能
IE9不支持文件导入导出
IE10不支持拖拽文件导入
九、UIAutomation—IOS自动化测试的工具
xcode中自带的Instuments工具可以用来进行APP的自动化测试, 以及用于进行内存泄露, 文件读写操作等的性能分析.
第一部分: 熟悉Instruments的UIAutomation.
首先, 选取xcode->Open Developer Tool->Instruments打开Instruments工具, 然后在左上角可以选取设备及被测APP如下图):
在这里, 我选取了iPhone 5s的一个模拟浓ky”/kf/ware/vc/” target=”_blank” class=”keylink”>vcsINLUvLDWrsewseDQtLXE0ru49rzytaW1xNaquvXI1bGoQVBQLjwvcD4KPHA+uNW/qsq8vdO0pVVJQXV0b21hdGlvbrXEu7AsIL2o0unRodTxwrzWxr3Fsb61xLe9yr3AtMrsz6S4w7mkvt+1xMq508MuIMjnz8LNvLXEtdeyv7XEyP249rC0xaW31rHwyseypbfFLCDCvNbGLCDNo9a5LjwvcD4KPHA+PGltZyBzcmM9″/uploadfile/Collfiles/20141229/20141229084253128.png” alt=””>
点击中间的红色按钮开始录制, 录制过程中, 代码框中会实时更新自动化脚本, 点击停止后, 就录制成功了一段自动化测试脚本了. 然后可以点击左边执行即可看到iPhone 5s模拟器中的执行结果了, 跟录制的动作是一致的.
可以看出, 使用UIAutomation对IOS的APP进行自动化测试, 使用的是JavaScript语言.
以上的target, app是建立特定的执行环境, 然后通过app.mainWindow)获取APP的UIWindow.
也可以通过var navBar = app.navigationBar)来获取APP的导航栏navigationBar.
使用target.logElementTree)可以建立APP的层级结构树类似于Android自动化中的getHierarchyView)方法). 一个简单的结构如下:
第二部分: 控件的获取及操作
获取UI控件的方法也非常简便:
其他控件的获取都是类似的方法, 如buttons), images), webViews).
对控件的操作如下:
导航栏navigationBar与tabBar的获取及操作如下:
打印调试log的方式如下:
第三部分: 自定义自动化脚本
熟悉了基本的UIAutomation相关的规则之后, 我们就可以来编写自定义的自动化脚本了.
在这里, 我简单的取出tableView上的所有cell, 并依次点击该cell, 然后跳转至每个cell的详细界面, 最后返回.
log栏里, 会呈现所有的执行结果, 分析起来也是非常方便的.
怎么样, 使用起来是不是蛮简单的. 但是, 在这里, 只是简单总结了Instruments中UIAutomation的基本用法, 真正精髓的东西还要自己去慢慢琢磨.
所有的技术都是易学难精, 大家加油.
十、【iOS小白教程】开始测试:单元测试与UI测试
单元测试适合测试用户交互行为无法覆盖的代码,和小而完整的代码。UI 测试更适合测试大范围的功能集合。
一、单元测试
iOS 单元测试和 UI 测试快速入门:非常适合小白的入门教程,code
iOS测试各个断言用法:工具而已
用 Xcode 进行单元测试
创建一个单元测试 Target
Xcode 的测试导航器提供了一种最简单的进行测试的方法;你可以用它创建一个测试 target 并在你的 app 中进行测试。
打开 BullsEye 项目,按下 command+5 打开它的测试导航器。
点击左下角的 + 按钮,然后从菜单中选择 New Unit Test Target…:
使用默认的 BullsEyeTests 作为名字。当导航器中显示出测试 bundle 时,点击并在编辑器中打开它。如果 BullsEyeTests 没有自动显示,点击其它导航器,然后返回测试导航器。
模板代码中,import 了 XCTest,并定义了一个 XCTestCase 的继承类 BullsEyeTests,并声明了 setup)、tearDown) 和 示例测试方法。
有三种运行这个测试类的方法:
ProductTest 或者 Command-U。这实际上会运行所有测试类。
点击测试导航器中的箭头按钮。
点击中缝上的钻石图标。
你还可以点击某个测试方法上的钻石按钮单独测试这个方法,钻石按钮在导航器和中缝上都有。
测试不同的运行测试方法,看看它们会运行多长时间,以及运行起来的样子。示例测试不会执行任何动作,因此它们的运行是十分快速的!
如果所有测试通过,钻石会变绿并显示一个对勾。点击 testPerformanceExample) 底部的灰色钻石,打开 Performance Result:
你用不着 testPerformanceExample) 方法,请删除它。
用 XCTAssert 测试模型
首先,用 XCTAssert 测试 BullsEye 的模型中的一个核心功能:一个 BullsEyeGame 对象能够正确计算出一局游戏的得分吗?
在 BullsEyeTests.swift 中,在 import 语句下面添加:
@testable import BullsEye
1
这样单元测试就可以访问 BullsEye 中的类和方法了。
在 BullsEyeTests 类头部加入一个属性:
var gameUnderTest: BullsEyeGame!
1
在 setup) 方法中创建一个新的 BullsEyeGame 对象,位于 super 方法调用之后:
gameUnderTest = BullsEyeGame)
gameUnderTest.startNewGame)
1
2
这会用类的级别创建出一个 SUT(被测系统)对象,因此这个测试类中的所有测试都能够访问 SUT 对象的属性和方法。
这里,你也调用了游戏的 startNewGame 方法,这会创建一个 targetValue。你会有很多测试都要使用 targetValue,以测试游戏中计算的得分是否正确。
别忘了在 tearDown) 方法中释放你的 SUT 对象,在调用 super 方法之前:
gameUnderTest = nil
1
注意:在 setup) 中创建 SUT,在 tearDown) 中释放 SUT 是一种好的做法,能够保证每次测试都以干净的状态开始。更多讨论,请阅读 Jon Reid 的这篇帖子。
准备编写你的第一个测试了!
将 testExample) 方法修改为:
// 用 XCTAssert 测试模型
func testScoreIsComputed) {
// 1. given
let guess = gameUnderTest.targetValue + 5
// 2. when
_ = gameUnderTest.checkguess: guess)
// 3. then
XCTAssertEqualgameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}
1
2
3
4
5
6
7
8
9
10
11
测试方法的名字总是以 test 开头,后面加上一个对测试内容的描述。
将测试方法分成 given、when 和 then 三个部分是一种好的做法:
在 given 节,应该给出要计算的值:在这里,我们给出了一个猜测数,你可以指定它和 targetValue 相差多少。
在 when 节,执行要测试的代码,调用 gameUnderTest.check_:)方法。
在 then 节,将结果和你期望的值进行断言(这里,gameUnderTest.scoreRound 应该是 100-5),如果测试失败,打印指定的消息。
点击中缝上或者测试导航器上的钻石图标。App 会编译并运行,钻石图标会变成绿色的对勾!
注意:要查看完整 XCTestAssertions 列表,Command+左键,点击 XCTAssertEqual,将跳到 XCTestAssertions.h,或者通过阅读苹果的 Assertions Listed by Category。
注意:Given-When-Then 结构源自 BDD行为驱动开发),是一个对客户端友好的、更少专业术语的叫法。另外也可以叫做 Arrange-Act-Assert 和 Assemble-Activate-Assert。
在测试中进行 debug
在 BullsEyeGame 中认为制造了一个 bug,你可以试试怎么找出它。将 testScoreIsComputed 修改为 testScoreIsComputedWhenGuessGTTarget,然后复制、粘贴,将它复制为另外一个方法 testScoreIsComputedWhenGuessLTTarget。
在这个方法中,在 given 节,让 targetValue – 5,而其它地方不变:
func testScoreIsComputedWhenGuessLTTarget) {
// 1. given
let guess = gameUnderTest.targetValue - 5
// 2. when
_ = gameUnderTest.checkguess: guess)
// 3. then
XCTAssertEqualgameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}
1
2
3
4
5
6
7
8
9
10
猜的数和正确的数仍然相差 5,因此得分仍然应该是 95。
在断点导航器中,添加一个 Test Failure 断点,这样,当测试方法断言失败时,测试会停止。
运行测试,它会在 XCTAssertEqual 这行停止,同时报告测试失败。
查看 debug 控制台中的 gameUnderTest 和 guess 值:
guess 变量是 targetValue – 5,但 scoreRound 是 105,而不是 95!
接下来,用正常的调试方法进行调试:分别在 when 节的代码上设置一个断点,在 BullsEyeGame.swift 的 check_:) 方法的声明 difference 变量处设置一个断点。再次运行测试,跳到 let difference 语句处,观察 difference 变量的值:
问题是: difference 是负值,因此 score 变成了 100 – -5); 解决办法是在 difference 上取绝对值。在 check_:) 方法中,反注释正确的代码,删掉错误的代码。
清除两个断点,再次运行测试确保测试通过。
用 XCTestExpectation 测试异步操作
现在我们学习了如何测试模型,如何对测试失败的情况进行 debug,接下来介绍用 XCTestExpectation 来测试网络操作。
打开 HalfTunes 项目:它使用 URLSession 来查询 iTunes API,下载歌曲小样。假设你想用 AlamoFire 来进行网络请求。为了证明一切正常,你应该为网络请求编写测试,并在修改代码之前进行测试。
URLSession 方法是异步的:它们会立即返回,但并不会终止运行直到未来某个时候。要测试异步方法,需要用 XCTestExpectation 以确保测试会等待异步操作完成。
异步测试通常是慢的,因此要和其它较快的单元测试分开来。
点 + 按钮,选择 New Unit Test Target… ,取名为 HalfTunesSlowTests。在 import 语句中,导入 HalfTunes:
@testable import HalfTunes
1
这个类中的测试方法会使用默认的 session 来向苹果服务器发送请求,因此声明一个 sessionUnderTest 对象,在 setup) 方法中实例化而在 tearDown) 中释放这个对象。
var sessionUnderTest: URLSession!
override func setUp) {
super.setUp)
sessionUnderTest = URLSessionconfiguration: URLSessionConfiguration.default)
}
override func tearDown) {
sessionUnderTest = nil
super.tearDown)
}
1
2
3
4
5
6
7
8
9
10
11
将 testExample) 方法修改为异步测试方法:
// 异步测试: 成功块,失败慢
func testValidCallToiTunesGetsHTTPStatusCode200) {
// given
let url = URLstring: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
// 1
let promise = expectationdescription: "Status code: 200")
// when
let dataTask = sessionUnderTest.dataTaskwith: url!) { data, response, error in
// then
if let error = error {
XCTFail"Error: error.localizedDescription)")
return
} else if let statusCode = response as? HTTPURLResponse)?.statusCode {
if statusCode == 200 {
// 2
promise.fulfill)
} else {
XCTFail"Status code: statusCode)")
}
}
}
dataTask.resume)
// 3
waitForExpectationstimeout: 5, handler: nil)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这个方法检查 iTunes 返回状态码是否为 200。大部分代码和你在 App 中的写法一样,除了这几行:
expectation_:) 返回一个 XCTestExpectation 对象,这个对象保存了一个承诺(promise),又称作期望expectation)或未来future)。参数 description 用于描述你预期的行为。
为了和描述一致,你可以在异步方法的成功回调块中调用 promise.fullfill)。
waitForExpections:_handler:) 保持测试方法的运行,直到所有的 expectation 被 fullfill 或者到达指定超时时间,这两个条件任何一个都行。
运行测试。如果已连接网络,当模拟器中 app 启动之后,测试很快就会通过,
更快的失败
失败是痛苦的,但它不一定会发生。那么如何才能快速模拟测试失败的方法?这样我们才可以节省时间用于浪费到 Facebook 上?:]
可以修改你的代码,以便模拟异步操作失败的情况,将 URL 中 itunes 删除最后的 s:
let url = URLstring: "https://itune.apple.com/search?media=music&entity=song&term=abba")
1
运行测试:如果失败,app 会在超时时间之后才返回!这是因为它的 expectation 是请求成功,我们也只有在成功时才调用 promise.fullfil)。因此当操作失败时,测试要结束必须等超时时间过去。
你可以修改 expectation,让测试失败得更快一点:
我们不要去等待请求成功了,而是去等待异步方法完成块被回调的时候。这会在 app 收到响应之后立即发生——无论服务器返回的是 OK 还是错误,expectation 都会 fulfill。这样,无论请求是否成功,你都能检测到。
我们创建一个新的测试方法来进行测试。首先,将修改过的 url 恢复原样,然后新建一个测试方法:
// 异步测试: 让失败更快
func testCallToiTunesCompletes) {
// given
let url = URLstring: "https://itune.apple.com/search?media=music&entity=song&term=abba")
// 1
let promise = expectationdescription: "Completion handler invoked")
var statusCode: Int?
var responseError: Error?
// when
let dataTask = sessionUnderTest.dataTaskwith: url!) { data, response, error in
statusCode = response as? HTTPURLResponse)?.statusCode
responseError = error
// 2
promise.fulfill)
}
dataTask.resume)
// 3
waitForExpectationstimeout: 5, handler: nil)
// then
XCTAssertNilresponseError)
XCTAssertEqualstatusCode, 200)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
关键的一点是,在完成回调中,简单地 fulfill 这个 expectation,这个过程会很快。如果请求失败,断言就会失败。
运行测试,现在失败的时间只需要一秒,这次的失败是真的因为请求失败,而不是因为超时。
恢复 url,再次进行测试,请求仍然是成功的。
模拟对象和交互
异步测试让你在编写调用异步 API 时更能保证输入的正确。你可能还想测试一下用 URLSession 获取数据,或者是更新 Userdefaults 或 CloudKit 数据库的代码是否正确。
大部分 App 会和系统或库对象打交道——你无法控制这些对象——和这些对象交互时测试会变慢和不可重现,这违背了 FIRST 原则的其中两条。但是,你可以通过从存根获取数据或者写入模拟对象写入来模拟这种交互。
当你的代码需要依赖系统或库对象时可以使用伪造手段——创建一个模拟的对象代替并将之注入到你的代码中。Jon Reid 的“依赖注入”一文介绍了几种方法。
模拟从存根获取数据
在这个测试方法中,你会测试 app 的 updateSearchResults_:) 方法是否能够解析从 session 中下载的数据,通过检查 searchResults.cout 是否正确的方式。这个 SUT 是 view controller,你将用存根和已经下载好的数据来模仿 session。
点击 +,选择 New Unit Test Target…,取名为 HalfTunesFakeTest。在 import 语句中导入 HalfTunes:
@testable import HalfTunes
1
声明这个 SUT,在 setup) 方法中构建,在 tearDown) 方法中释放:
var controllerUnderTest: SearchViewController!
override func setUp) {
super.setUp)
controllerUnderTest = UIStoryboardname: "Main",
bundle: nil).instantiateInitialViewController) as! SearchViewController!
}
override func tearDown) {
controllerUnderTest = nil
super.tearDown)
}
1
2
3
4
5
6
7
8
9
10
11
12
注意:SUT 是这个 view contoller,因为 HalfTunes 有一个很大的问题——所有的工作都是在 SearchViewController.swift 中进行的。将网络代码迁移到一个独立的模块中能够解决这个问题,并能使测试更容易进行。
然后,你需要一些测试的 JSON 数据,这些数据将通过伪造的 session 来提供给测试方法。只需要几个数据就可以了,你可以在 URL 字符串中用 &limit=3 来限制结果集:
https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3
1
将这个 URL 复制到浏览器中。将结果下载到一个 1.txt 之类的文件中。打开它,检查它的 JSON 格式,然后重命名为 abbaData.json,然后将它拖到 HalfTunesFakeTests 的文件组中。
HalfTunes 项目中有一个 DHURLSessionMock.swift 文件。它定义了一个简单的协议 DHURLSession,包含了用 URL 或者 URLRequest 来创建 data taks 的方法。还有实现了这个协议的 URLSessionMock 类,它的初始化方法允许你用指定的数据、response 和 error 来创建一个伪造的 URLSession。
构造模拟数据和 response,然后创建一个伪造的 seesion 对象,就在 setup) 方法中创建完 SUT 对象后面:
let testBundle = Bundlefor: typeof: self))
let path = testBundle.pathforResource: "abbaData", ofType: "json")
let data = try? DatacontentsOf: URLfileURLWithPath: path!), options: .alwaysMapped)
let url = URLstring: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let urlResponse = HTTPURLResponseurl: url!, statusCode: 200, httpVersion: nil, headerFields: nil)
let sessionMock = URLSessionMockdata: data, response: urlResponse, error: nil)
1
2
3
4
5
6
7
8
在 setup) 最后,将这个伪造的 session 注入到 app 的属性中:
controllerUnderTest.defaultSession = sessionMock
1
注意:你也可以直接在测试方法中使用这个伪造的 session,但我们想演示依赖注入,这样你就可以通过 view controller 的 defaultSession 属性来测试 SUT 的方法.
接下来准备编写测试方法,调用 updateSearchResults_:) 方法来解析模拟数据。将 testExample) 方法替换为:
// 用 DHURLSession 协议和模拟数据伪造 URLSession
func test_UpdateSearchResults_ParsesData) {
// given
let promise = expectationdescription: "Status code: 200")
// when
XCTAssertEqualcontrollerUnderTest?.searchResults.count, 0, "searchResults should be empty before the data task runs")
let url = URLstring: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let dataTask = controllerUnderTest?.defaultSession.dataTaskwith: url!) {
data, response, error in
// 如果 HTTP 请求成功,调用 updateSearchResults_:) 方法,它会将数据解析成 Tracks 对象
if let error = error {
printerror.localizedDescription)
} else if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 200 {
promise.fulfill)
self.controllerUnderTest?.updateSearchResultsdata)
}
}
}
dataTask?.resume)
waitForExpectationstimeout: 5, handler: nil)
// then
XCTAssertEqualcontrollerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
我们仍然要编写异步测试的代码,因为存根是也模拟了异步方法。
当 data task 还没有执行,断言 searchResults 为空是成立的——即断言为 true,因为在 setup) 方法中我们创建的是一个全新的 SUT。
模拟数据中包含了 3 个 Track 对象,因此我们断言 view controller 的 searchResults 数组中包含了 3 个对象。
运行测试。很快就通过测试,因为根本就没有使用网络连接!
模拟写入 mock 对象
上一个测试使用了 stub 存根)来从伪造对象中获取数据。接下来,你将用一个伪造对象来测试向 UserDefaults 进行写入的代码是否正确。
再次打开 BullsEye 项目。这个 App 有两种游戏方式:用户可以拖动 slider 来猜数字,也可以通过 slider 的位置来猜数字。右下角的 segmented 控件可以切换游戏方式,并将 gameStyle 保存到 UserDefaults。
这个测试将检查 app 是否正确地保存了 gameStyle 到 UserDefaults 里。
在测试导航器中,点击 New Unit Test Target… ,取名为 BullsEyeMockTests。在 import 语句下添加:
@testable import BullsEye
class MockUserDefaults: UserDefaults {
var gameStyleChanged = 0
override func set_ value: Int, forKey defaultName: String) {
if defaultName == "gameStyle" {
gameStyleChanged += 1
}
}
}
1
2
3
4
5
6
7
8
9
10
MockUserDefaults 重写了 set_:forKey:) 方法,用于增加 gameStyleChanged 的值。通常你可能认为应当使用 Bool 变量,但使用 Int 能带来更多的好处——例如,在你的测试中你可以检查这个方法是否真的被调用过一次。
在 BullsEyeMockTests 中声明 SULT 和 MockUserDefaults 对象:
var controllerUnderTest: ViewController!
var mockUserDefaults: MockUserDefaults!
1
2
在 setup) 方法中,创建 SUT 和伪造对象,然后将伪造对象注入到 SUT 的属性中:
controllerUnderTest = UIStoryboardname: "Main", bundle: nil).instantiateInitialViewController) as! ViewController!
mockUserDefaults = MockUserDefaultssuiteName: "testing")!
controllerUnderTest.defaults = mockUserDefaults
1
2
3
在 tearDown) 中释放 SUT 和伪造对象:
controllerUnderTest = nil
mockUserDefaults = nil
1
2
将 testExample) 替换为:
// 模拟和 UserDefaults 的交互
func testGameStyleCanBeChanged) {
// given
let segmentedControl = UISegmentedControl)
// when
XCTAssertEqualmockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions")
segmentedControl.addTargetcontrollerUnderTest,
action: #selectorViewController.chooseGameStyle_:)), for: .valueChanged)
segmentedControl.sendActionsfor: .valueChanged)
// then
XCTAssertEqualmockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
在测试方法“点击” segmented 控件之前,when 断言中,gameStyleChanged 的值应当是 0。在 then 断言中也是 true,因为 set:forkey) 方法真的被调用了 1 次。
运行测试,测试通过。
XCode 的 UI 测试
从 Xcode 7 开始引入了 UI 测试,允许你通过记录 UI 上的交互来创建 UI 测试。UI 测试通过查询来找到 app 的 UI 对象,同步事件,然后将事件发送给这些对象。这个 API 允许你检索 UI 对象的属性和状态,并和预期值进行比对。
在 BullsEye 项目的测试导航器中,添加一个新的 UI Test Target。在 Target to be Tested 勾上 BullsEye,名称保持默认的 BullsEyeUITests。
在 BullsEyeUITest 类中添加一个属性:
var app: XCUIApplication!
1
在 setup) 方法,将 XCUIApplication).launch) 一句替换为:
app = XCUIApplication)
app.launch)
1
2
将 testExample) 方法名修改为 testGameStyleSwitch)。
在 testGameStyleSwitch) 中新起一行,然后在底部的编辑器窗口中,点击红色的 Record 按钮。
当模拟器中 App 一打开,点击游戏方式切换的 segmented 控件的 Slide 按钮和顶部标签。然后点击 Xcode 中的 Record 按钮,停止录制。
在 testGameStyleSwitch) 方法中会添加三行代码:
let app = XCUIApplication)
app.buttons["Slide"].tap)
app.staticTexts["Get as close as you can to: "].tap)
1
2
3
如果还有其它语句,请删除。
第一行代码完全和 setup) 方法中的代码重复了,同时你也不需要真的点击动作,因此可以删除第一行和其它两句的 .tap)。点开[“slide”]旁边的下拉菜单,选择 segsegmentedControls.buttons[“Slide”]。
这样就变成了:
app.segmentedControls.buttons["Slide"]
app.staticTexts["Get as close as you can to: "]
1
2
然后开始编写 given 节:
// given
let slideButton = app.segmentedControls.buttons["Slide"]
let typeButton = app.segmentedControls.buttons["Type"]
let slideLabel = app.staticTexts["Get as close as you can to: "]
let typeLabel = app.staticTexts["Guess where the slider is: "]
1
2
3
4
5
现在你已经获得两个按钮和两个顶部标签的引用,继续添加:
// then
if slideButton.isSelected {
XCTAssertTrueslideLabel.exists)
XCTAssertFalsetypeLabel.exists)
typeButton.tap)
XCTAssertTruetypeLabel.exists)
XCTAssertFalseslideLabel.exists)
} else if typeButton.isSelected {
XCTAssertTruetypeLabel.exists)
XCTAssertFalseslideLabel.exists)
slideButton.tap)
XCTAssertTrueslideLabel.exists)
XCTAssertFalsetypeLabel.exists)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这是为了测试当任何一个按钮被选中或点击时,对应的标签是否存在。运行测试,断言应当成立。
性能测试
根据苹果文档描述:性能测试将一段代码执行十遍,计算出平均执行时间和标准偏差。平均值用于和基线值进行比较,以衡量是否测试通过。
编写性能测试十分简单:将需要测试的代码放进 measure) 方法的闭包中。
要实际体验一把,可以打开 HalfTunes 项目,将 HalfTunesFakeTests 的 testPerformanceExample) 方法替换为:
// 性能测试
func test_StartDownload_Performance) {
let track = Trackname: "Waterloo", artist: "ABBA",
previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a")
measure {
self.controllerUnderTest?.startDownloadtrack)
}
}
1
2
3
4
5
6
7
8
运行测试,点击 measure) 方法闭包结束 } 旁边的图标,查看统计结果。
点击 Set Baseline,再次运行测试,查看结果——结果可能比基线要好或差。 Edit 按钮允许你修改基线以查看新的报告。
基线是根据设备配置来保存的,因此你可以在不同设备上进行同一个测试,每种设备会根据其配置的处理器速度、内存而拥有不同的基线。
当你对 App 进行修改之后,都会对测试性能造成影响,请重新运行性能测试,查看时需要参照基线进行。
代码覆盖
代码覆盖工具会报告 App 代码有多少经过了测试,你也可以知道那一部分代码还没有经过测试。
注意:在代码覆盖选项打开的情况下,可以运行性能测试吗?根据苹果文档中描述:代码覆盖率统计会导致性能下降……它以线型方式影响代码的执行,因此当同样是在打开的情况下,一次性能测试的结果和另外一次性能测试的结果仍然是有可比性的。但是,如果要在测试中进行严谨的性能计算,你应当考虑是否要开启代码覆盖。
要打开代码覆盖,编辑 scheme 中的 Test,然后勾选 Code Coverage 选项:
运行全部测试(command+U),打开报告导航器(command+8)。选择 By Time,选择列表中第一个,然后选择 Coverage 栏:
点击倒三角,查看 SearchViewController.swift 的函数列表:
将鼠标放到 updateSearchResults:) 旁边的蓝色覆盖率统计条上,可以看到覆盖率为 71.88%。
点击这个函数的箭头按钮,打开源文件,找到该函数。将鼠标靠近右边缝的覆盖率标注上,代码会被分成不同颜色的部分,红色或者绿色:
股概率标注显示了一个测试“击中”了代码多少次,没有被调用过的代码会用红色标注。你可能猜到了,for 循环被执行了 3 次,而错误路径一次都没执行。为了提高这个函数的覆盖率,你应该复制 abbaData.json,修改内容使它产生错误——比如,将“results”修改为“result”,从而导致这句:print“Results key not found in dictionary”)被调用。
100% 的覆盖率
要追求 100% 的覆盖率有多难?你可以用谷歌搜一下 100% unit test coverage,你会发现有许多赞成和反对的意见,以及关于对 100% 股概率的定义的争论。反对的一方认为至少有 10-15% 的覆盖率是毫无意义的。赞成的一方认为最后的 10-15% 是最重要的,因为它们很难被测试出来。再搜一下 “hard to unit test bad design”,你会发现一种比较有说服力的说法,不能测试的代码表明存在深层次的设计问题。进一步的思考表明,测试驱动开发才是正理。
结束
你拥有了几个帮助你编写测试的良好工具。我希望本教程能够在你测试任何事情的时候充满自信。
在这里瞎子完整项目的zip 文档。
要学习更多内容,请参考如下资源:
现在你是自己编写测试的,更进一步是自动化:持续集成和持续分发。首先是苹果的关于 Xcode 服务器和 xcodebuild 的官方文档 Automating the Test Process,维基百科关于持续分发,图来自于 ThoughtWorks。
TDD in Swift Playgrounds 利用 XCTestObservationCenter 在 playground 中运行 XCTestCase 单元测试。你可以在 playground 中开发和测试项目代码,然后将它们转换到 app 中。
CMD+U Conference 的 Watch Apps: How Do We Test Them? ,使用 PivotalCoreKit 来测试 watchOS apps。
如果你的 App 还未编写过任何测试,你可以参考 Michael Feathers 的 Working Effectively with Legacy Code ,因为没有测试过的代码是遗留问题。
Jon Reid 的 Quality Coding 的示例 App app 是一个学习测试驱动开发的好去处。
十一、IOS自动化之KIF框架实践
KIF的全称是Keep it functional。它是一个建立在XCTest的UI测试框架,通过accessibility来定位具体的控件,再利用私有的API来操作UI。由于是建立在XCTest上的,所以你可以完美的借助XCode的测试相关工具。
一, 测试环境搭建
KIF框架依赖工程源码进行测试的,所以要能从开发处拿到被测试工程的源码,然后在搭建相应的测试环境。
1,命令行安装pod:
sudo gem install cocoapods
2,修改或创建工程的pod文件 :Podfile,如下所示:
# platform :ios, ‘9.0’
target ‘TenMinDemo’ do
# Uncomment the next line if you’re using Swift or would like to use dynamic frameworks
# use_frameworks!
# Pods for CYXText
pod ‘AFNetworking’, ‘~> 3.0’
pod “SDWebImage”
pod “MJExtension”
pod “MJRefresh”
pod ‘SVProgressHUD’
pod ‘ReactiveObjC’
end
target ‘TenMinDemoTests’ do
use_frameworks!
pod ‘KIF’, ‘~> 3.5.1’
end
其中如下一段内容为新添加的:
target ‘TenMinDemoTests’ do //测试工程名
use_frameworks!
pod ‘KIF’, ‘~> 3.5.1’
end
3,执行安装命令:
pod install
安装相应的内容。
4,在现有工程中添加Target实现
选择File→New→Target…菜单项, 从中选择iOS→Other中的Cocoa Touch Unit Testing Bundle模板.如下图所示:
单击下一步,进行相应的设置页:
5,设置测试工程相关项:
(1) Product Name:KIF测试工程名,可以自由命名,最好是测试工程名+”Tests”。
(2)Organization Name, Organization identifier, Bundle identifier,根据需要自行全名即可。
(3)Language:编码语言,有Objective-C和Swift,默认选择OC.
(4)Project和Target to be Tested:为对应的要测试的工程名,一定要保证是正确的。
(5)单击“Finish”创建完成。
(6)工程创建完成,如下所示:
生成TenMinDemoTests工程,同时生成TenMinDemoTests.m文件和info.plist文件。
在Products文件夹中生成“TenMinDemoTests.xctest”文件。
TenMinDemoTests.m文件内容如下:
// TenMinDemoTestsB.m
// TenMinDemoTestsB
//
// Created by GrowingIO on 2018/5/30.
// Copyright © 2018年 SXF. All rights reserved.
//
#import <XCTest/XCTest.h>
@interface TenMinDemoTests : XCTestCase
@end
@implementation TenMinDemoTests
– void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}
– void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}
– void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
– void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
@end
各个函数相互作用及执行顺序如下图所示:
二, 测试用例编写
KIF和其他测试框架类似,通过OC编写代码实现我们的测试步骤,进而去检测操作完成的结果。下面我们以一个简单而完整的用例来说明一下测试用例如何编写:
1,手工用例步骤介绍:
(1)我从网上下载的一个Demo,工程名为TenMinDemo,选择其中的一个功能,登录:打开app,选择“博文”项
(2)输入用户名:dingdone和密码:123456,单击“登录”按钮,登录成功。
(3)检测是否进入到了列表页。
2,代码测试用例如下:
//LoginTests.m
//
// LoginTest.m
// TenMinDemoTests
//
// Created by GrowingIO on 2018/5/31.
// Copyright © 2018年 SXF. All rights reserved.
//
#import “LoginTest.h”
@implementation LoginTest
– void)setUp {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
– void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
– void)testLogin {
//单击“博文”
[tester tapViewWithAccessibilityLabel:@”博文”];
//输出用户名和密码
[tester clearTextFromViewWithAccessibilityLabel:@”usename”];
[tester enterText:@”dingdone
” intoViewWithAccessibilityLabel:@”usename”];
[tester clearTextFromViewWithAccessibilityLabel:@”password”];
[tester enterText:@”123456
” intoViewWithAccessibilityLabel:@”password”];
//等待1秒
[tester waitForTimeInterval:1];
//单击登录按钮
[tester tapViewWithAccessibilityLabel:@”login”];
[tester waitForTimeInterval:5];
//获取列表页view
UITableView *utable=[tester waitForViewWithAccessibilityLabel:@”tableView”];
//获取tableview第二行
NSIndexPath *index = [NSIndexPath indexPathForRow:1 inSection:0];
UITableViewCell *utvc=[tester waitForCellAtIndexPath:index inTableView:utable];
if @availableiOS 11.0, *)) {
//判断第二行的内容是否符合预期
//NSLog@”Cell Content:%@”,utvc.textLabel.text);
XCTAssertEqualObjectsutvc.textLabel.text, @”第2行”);
} else {
// Fallback on earlier versions
}
}
@end
上面的代码添加了详细的注释,内容也比较简单,就不逐步介绍了。注意:KIF是基于XCText的,基本的判断语句都是XCTest的。
3,KIF API简介
KIF API相关的介绍挺多,只是不太全面,下面我介绍几个不错的网站:
(1)KIF API的中文翻译https://www.jianshu.com/p/87bbbb798926,
感觉好像不太全;
(2)官网上的源码及其介绍:https://github.com/kif-framework/KIF.
(3) KIFUITestActor Doc:http://cocoadocs.org/docsets/KIF/3.2.1/Classes/KIFUITestActor.html,这个网站内容比较全面,只是英文的,不FQ能不能打开还不清楚。
很多新的技术都有一个特点,就是相关的文档非常少,这也没有办法;可是换个想法来看,如果相关技术文档到处都是,这项技术学了也没有什么优势了。
三,测试用例集及命令行执行
目前KIF一个Kiftestcase类就相当于一个测试用例集,我们可以简单地利用类的方式来组织测试用例,类中的每个以test****开头的函数均是一个测试用例。当然基于KIF开源的特性,我们可以开发其他的用例集管理方式或是工具,如下图所示:
(图片来源于:https://tech.meituan.com/iOS-UITest-KIF.html)
任何自动化测试最终的归宿都是CI持续化集成),当然我们的KIF自动化也需要做持续化集成。而在做持续化集成前,需要调研如何通过命令行来执行测试用例?
(1)将项目设置成shared
从product->Scheme->manage schemes,查看项目是否是shared,如果不是,则选中后面的复选框将其共享。
(2)借助于xctool来执行测试用例
Xctool源码地址:https://github.com/facebook/xctool,可以去查看一下如何安装和使用:https://blog.csdn.net/jeikerxiao/article/details/51669451
而运行我们的示例代码应该是:
xctool -workspace XXX.xcworkspace –schem XXYYY run-tests XXXtests:测试用例集 -sdk “iphonesimulator11.3”
同时可以通过-only来指定运行某个用例
a)运行单个测试用例
xctool -workspace TenMinDemo.xcworkspace -scheme TenMinDemo run-tests -only TenMinDemoTests:LoginTest/testlogin -sdk “iphonesimulator11.3”
b)运行一个测试用例集
xctool -workspace TenMinDemo.xcworkspace -scheme TenMinDemo run-tests TenMinDemoTests:LoginTest -sdk “iphonesimulator11.3”
四,生成测试报告
xctool的reporter选项可以生成不同形式的测试报告:
pretty: 默认) 一个文字化的输出器,使用 ANSI 颜色和 unicode 符号来进行美化输出。
plain: 类似 pretty, 不过没有颜色和 Unicode。
phabricator: 把构建/测试的结果输出为 JSON 数组,它可以被 Phabricator 的代码评审工具读取。
junit: 把测试结果输出成和 JUnit/xUnit 兼容的 XML 文件。
json-stream: 一个由构建/测试事件组成的 JSON 字典流,每行一个(示例输出)。
json-compilation-database: 输出构建事件的 JSON Compilation Database ,它可以用于基于 Clang Tooling 的工具,例如 OCLint.
而我们通常使用xml格式测试报告,一则做持续化集成的时候,Jenkins可以直接这个报告;二则我们可以借助于ant将期转换成 html的报告:
1,生成junit报告:
xctool -workspace TenMinDemo.xcworkspace -scheme TenMinDemo run-tests TenMinDemoTests:LoginTest -sdk “iphonesimulator11.3” -reporter junit:/Users/growingio/Documents/report.xml
2,转换测试报告xml成html
由于xctool输出的报告中含有中间输出信息,所以不能直接转换,需要先处理一下,去掉无用的信息。或是先将报告转化成json格式,然后再提取出合适的信息,生成xml文件。这个方法需要另外写脚本转换一下。
生成报告的步骤如下:
(1)安装ant
(2)优化xctool生成的xml,把中间输出信息去掉
(3)创建builld.xml文件
<project name=”TestNG_WORKSPACE” default=”junit-report” basedir=”.”>
<!– Sets the property variables to point to respective directories –>
<property name=”junit-xml-dir” value=”${basedir}/junitreports”/> //junit报告文件的路径
<property name=”report-dir” value=”${basedir}/html-report” /> //生成html生成报告的位置
<!– Ant target to generate html report –>
<target name=”junit-report”>
<!– Delete and recreate the html report directories –>
<delete dir=”${report-dir}” failοnerrοr=”false”/>
<mkdir dir=”${report-dir}” />
<mkdir dir=”${report-dir}/Junit” />
<!– Ant task to generate the html report.
todir – Directory to generate the output reports
fileset – Directory to look for the junit xml reports.
report – defines the type of format to be generated.
Here we are using “noframes” which generates a single html report.
–>
<junitreport todir=”${report-dir}/Junit”>
<fileset dir=”${junit-xml-dir}”>
<include name=”**/*.xml” />
</fileset>
<report format=”noframes” todir=”${report-dir}/Junit” /> //format:”frames”,还左边框的报告,“noframes”,无边框的报告
</junitreport>
</target>
</project>
(4)将juntreport.xml的路径设置好,同时将build.xml给保存到相应的位置,然后在build.xml的路径下执行命令:ant,则会生成如下格式的html报告
五,持续化集成
持续化集成的核心思想就是借助于Jenkins的任务调度功能,将自动化测试用例放到Git/Svn上,然后配置相应的代码库地址,执行脚本命令等。根据不同的触发条件,完成自动运行的机制。其中还要根据需要进行相关的配置等等,网上相应的步骤比较多,在此就不详细介绍了,可以参考一下:“基于 KIF 的 iOS UI 自动化测试和持续集成(https://tech.meituan.com/iOS-UITest-KIF.html)”
在做持续化集成的时候,一定要考虑到因为环境原因,网络原因等造成的非正常失败的情况,故要加上用例失败重跑机制。
Commands如下所示:
xctool -workspace TenMinDemo.xcworkspace -scheme TenMinDemo run-tests TenMinDemoTests:LoginTest -sdk “iphonesimulator11.3” -reporter junit:/Users/growingio/Documents/report.xml
array= TimerTests HistoryTests )
for data in ${array[@]}
do
xctool -workspace TenMinDemo.xcworkspace -scheme TenMinDemo run-tests -only TenMinDemoTests:${data} -sdk “iphonesimulator11.3” -reporter junit:/Users/growingio/Documents/report.xml
done
六,总结
最近在公司以KIF框架做了一些IOS自动化测试相关的东西,中间磕磕碰碰的遇到了不少问题,也查了资料进行了解答。现在就整个项目的实现过程给总结一下,以方便知识的总结和记录,也有利于后来者进行快速入门。
由于经验有限,难免会存在不足之处,后续会不断更新和完善。