IOS自动化测试框架搭建及测试流程介绍(满满的干货)

一、认知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,理论上讲可以是任何一种语言。所以有了iOSDriverAppium.

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操作指令的执行。其中最著名的代表为FrankCalabash. 它们还是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自动化测试相关的东西,中间磕磕碰碰的遇到了不少问题,也查了资料进行了解答。现在就整个项目的实现过程给总结一下,以方便知识的总结和记录,也有利于后来者进行快速入门。
由于经验有限,难免会存在不足之处,后续会不断更新和完善。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注