基于 KIF 的 iOS UI 自动化测试和持续集成
sdfsa
8年前
<p>客户端 UI 自动化测试是大多数测试团队的研究重点,本文介绍猫眼测试团队在猫眼 iOS 客户端实践的基于 KIF 的 UI 自动化测试和持续集成过程。</p> <h2>一、测试框架的选择</h2> <p>iOS UI 自动化测试框架有不少,其中 UI Automation 是 Apple 早期提供的 UI 自动化测试解决方法,用 JavaScript 编写测试脚本,通过标签和值的可访问性获得 UI 元素,来完成相应的交互操作。</p> <p>一些第三方 UI 解决方案以 UI Automation 为基础,对其进行补充和优化,包括扩展型 UI Automation 和驱动型 UI Automation。</p> <ul> <li>扩展型 UI Automation 采用 JavaScript 扩展库方法提高 UI Automation 的易用性,常见的框架有 TuneupJs、ynm3k。</li> <li>驱动型 UI Automation 在自动化测试底层使用了 UI Automation 库,通过 TCP 等通信方式驱动 UI Automation 来完成自动化测试。这种方式下,编辑脚本的语言不再局限于 JavaScript 。常见的框架有 iOSDriver、Appium。</li> </ul> <p>还有一些其他的第三方解决方案,常见的框架类型有私有 API 型和注入编译型。</p> <ul> <li>私有 API 型框架直接使用 Apple 私有 API 对 UI 界面进行操作。常见的框架主要有 KIF。</li> <li>注入编译型框架在编译时注入一个 Server 到 App 内部,通过 Server 对外通信完成 UI 操作指令的执行。常见的框架有 Frank、Calabash。</li> </ul> <p>Xcode 7发布后,Apple 提供了一种新的 UI 自动化测试解决方法——UI Testing,它基于 XCTest 测试框架,通过控件的可访问性来定位和获取控件,并提供了多种 UI 操作 API,使用源码语言,能方便地进行调试。</p> <p>我们在以上分类中挑选具有代表性的自动化框架:UI Automation、Appium、KIF、Frank、UI Testing 进行对比,下表是这几种测试框架的特点对比:</p> <p><img src="https://simg.open-open.com/show/e7036f32d424e55d5a9bafa6432c5775.png"></p> <p>考虑选择测试框架的几种影响因素。首先,使用的语言和框架决定了测试人员的持续性学习成本,iOS 测试人员对 Object C 和 XCTest 熟悉和掌握程度高,不需要消耗额外的学习成本,人员更替时的接手成本也相对较低;其次,测试框架支持的 UI 操作的丰富性决定了测试用例的覆盖完整度,使用私有 API 的测试框架支持的 UI 操作较为全面,而同时支持 UIWebView 的测试框架则更占优势;另外,App 程序 UI 变化快,使用开发效率高、调试方便的测试框架能使我们在适应新 UI 变化、新需求时获得更小的投入产出比。</p> <p>综合以上考虑,KIF 框架已经展现了他的优势,并且 KIF 使用 XCTest 框架,使得其测试流程 iOS 程序的单测无异,可完全复用单测的持续集成流程,维护持续集成的成本相对降低;另外,KIF 是一个活跃的开源测试框架,可扩展性好,升级更新快,有活跃社区来探讨和解决使用过程中遇到的问题。鉴于上述优势,我们选择了 KIF 作为 iOS 的 UI 自动化测试框架。</p> <h2>二、KIF 自动化实施</h2> <p>KIF 利用 Apple 给所有控件提供的辅助属性 accessibility attributes 来定位和获取元素,完成界面的交互操作;结合使用 Xcode 的 XCTest 测试框架,拥有 XCTest 测试框架的特性,使得测试用例能以 command line build 工具运行并获取测试报告。</p> <p>下面介绍如何进行 KIF 自动化实施。</p> <h3>1. KIF 搭建</h3> <p>KIF 以第三方库的形式编译运行于工程中,搭建 KIF 之前,应该确保工程在 Xcode 上编译运行通过。</p> <p>KIF 基于 XCTest 框架,继承了 XCTest 的所有特性。和 XCTest 一样,我们首先应该在工程项目中创建基于 Cocoa Touch Testing Bundle 模板的 Target ,并确保创建的 Target 的属性有如下设置:</p> <ul> <li>“Build Phases”:设置 Target Dependencies , UI 自动化测试固然要依赖应用程序的 App 产物,所以需保证应用程序 Target 被添加在 Test Target 的 Target Dependencies 中。</li> <li>“Build Settings”:<br> 设置 “Bundle loader” 为:$(BUILT_PRODUCTS_DIR)/MyApp.app/MyApp;<br> 设置 “Test Host” 为:$(BUILT_PRODUCTS_DIR);<br> 设置 “Wrapper Extensions” 为:xctest。</li> </ul> <p>项目的设置准备好后,需要安装 KIF 库源码到项目。即可开始 KIF 编写用例之旅。</p> <p>KIF 通过属性值(AccessibilityLabel, AccessibilityIdentifier, AccessibilityTraits,Value...)在界面中定位元素。为了获取到目标元素,我们必须先设置元素的 accessibility 属性。如下,想要获取程序中一个列表的 cell 元素,我们给列表的 cell 控件设置 accessibility 属性(如左图所示),设置为“Section XX Row XX”,编译运行,即可获得历史列表的 cell 元素;用模拟器的 Accessibility Inspector 抓取到了这个历史列表元素(如右图所示):</p> <p><img src="https://simg.open-open.com/show/65b471a91d2e514a23327cf2db5bd798.png"></p> <p>KIF 为我们提供了对有 accessibility 属性控件的操作接口,如下最简单的两个操作接口:</p> <ul> <li>点击一个元素:- (void)tapViewWithAccessibilityLabel:(NSString *)label;</li> <li>等待一个元素的出现:- (UIView *)waitForViewWithAccessibilityLabel:(NSString *)label。</li> </ul> <p>在新建的 Target 同名目录下增加一个继承自 KIFTestCase 的类,类中编写我们的用例,完成对界面的点击和验证,如下:</p> <p><img src="https://simg.open-open.com/show/6e100fa0583d190eb0a0feeb181845a9.png"></p> <p>以上步骤都完成后, 基于KIF的简单用例便搭建完成,点击 Product->Test 或者快捷键 (⌘U) 即可看到我们的用例自动运行起来了。</p> <h3>2. 用例编写与组织</h3> <p>(1)accessibility 属性设置</p> <p>accessibility 属性是 Apple 给视觉障碍人群提供完全无障碍使用的基本属性,该属性表明了 UI 元素的可访问性、是什么、做什么以及会触发什么样的操作。原生的 UIKit 控件默认提供了这些信息,然而,自定义的控件则需要对该属性进行设置,设置方式可参考下面几点:</p> <ul> <li>设置方式:找到页面元素所属的代码文件,再到代码中找到该类的实现,在相应代码处添加其属性。</li> <li>查看方式:设置好后,开启模拟器的 Accessibility Inspector 功能,即可看到控件的 accessibility 属性。</li> <li>设置建议:设置的 AccessibilityLabel 属性值要有实际意义(用户可理解),因为设置这个属性后用户可以通过 VoiceOver 访问;用户不可访问的控件,比如某些放置控件的容器等应该设置为 AccessibilityIdentifier 。</li> </ul> <p>(2)用例常用操作接口:</p> <ul> <li>UI交互操作( KIFUITestActor.h 中可查阅):</li> </ul> <pre> <code class="language-objectivec">tapThisView: - (void)tapViewWithAccessibilityLabel:(NSString *)label; waitForView: - (UIView *)waitForViewWithAccessibilityLabel:(NSString *)label; 注意:函数返回了对应View的指针,可以对返回值取数据,从而进行一些判断 enterTextIntoView: - (void)enterText:(NSString *)text intoViewWithAccessibilityLabel:(NSString *)label; tapRowOnTableView: - (void)tapRowAtIndexPath:(NSIndexPath *)indexPath inTableViewWithAccessibilityIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(5_0); dismisses a system alert: - (void)acknowledgeSystemAlert;</code></pre> <p>扩展:我们还可以对 KIFUITestActor 类进行扩展,利用 KIFUITestActor 中的私有函数,使 AccessibilityIdentifier 代替 Label 识别元素,完成 tapThisView 、waitForView 等操作。</p> <ul> <li>用例集操作( KIFTestCase.h 中可查阅):</li> </ul> <pre> <code class="language-objectivec">- (void)beforeAll; 在本类中第一个 test case执行前执行一次 用处:执行本类中各个测试函数的公共操作 注意:因为不能保证这个方法与 test case 是同一个类实例,所以不能用来设置实例变量的值,但是可以设置静态变量 - (void)beforeEach; 在每一个 test case 执行前执行一次 用处:执行各个函数需要的测试环境 注意:因为确保这个方法与 test case 是同一个类实例,所以可以用来设置实例变量 - (void)afterEach; 在每一个 test case执行后执行一次 用处:用来将 App 恢复至 test case 之前的状态,可以包含一些条件判断逻辑,从失败的 test case 中恢复,以确保不影响之后的测试 - (void)afterAll; 执行完测试类的最后一个 test case 后执行一次 用处:用于将 App 恢复至测试的初始状态</code></pre> <ul> <li>系统的功能实现( KIFSystemTestActor.h 中可查阅):</li> </ul> <pre> <code class="language-objectivec">模拟用户旋转设备: - (void)simulateDeviceRotationToOrientation:(UIDeviceOrientation)orientation; 对当前屏幕截图并存储到硬盘中:- (void)captureScreenshotWithDescription:(NSString *)description;</code></pre> <p>(3)用例组织</p> <p>设计实现单个测试用例步骤如下:</p> <ul> <li>a. 设置测试所需要的环境;</li> <li>b. 测试用例的测试逻辑;</li> <li>c. 恢复App至此次测试前状态。</li> </ul> <p>a、c步骤可用 beforeEach、afterEach 来实现,这样保证了每个用例之间的独立性和用例运行的稳定性。</p> <p>一般来说,可将用例按功能分成若干个用例集,每个用例集按校验点或者功能点分成若干个用例,这样方便测试用例的管理和维护。 某些含有耗费时间多、耗费资源多的公共操作的用例可以集合成一个用例集,在用例集运行前统一执行。设计实现用例集步骤如下:</p> <ul> <li>a. 设置用例集需要的环境、公共操作;</li> <li>b. 设计各个用例;</li> <li>c. 恢复 App 至用例集测试的初始状态。</li> </ul> <p>a、c步骤可用 beforeAll、afterAll 来实现,下图展示了一个用例集的书写示例:</p> <pre> <code class="language-objectivec">#import "TimerTests.h" #import "KIFUITestActor+AccessibilityLabelAddition.h" #import "KIFUITestActor+IdentifierAdditions.h" #import "KIFUITestActor+TimerAdditions.h" @implementation TimerTests - (void)beforeAll { [tester setDebugModel]; } - (void)afterAll { [tester resetDebugModel]; [tester clearHistory]; } - (void)beforeEach { [tester setDebugModel]; } - (void)afterEach { [tester clearParams]; } - (void)testNameedTask { [tester enterText:@"myTask" intoViewWithAccessibilityLabel:@"Task Name Input"]; [tester enterWorktime:10 Breaktime:4 Repetitions:5]; [tester tapViewWithAccessibilityLabel:@"Start Working"]; [tester waitForViewWithAccessibilityLabel:@"myTask"]; [tester waitForViewWithAccessibilityLabel:@"Start Working"]; } - (void)testnoNameTask { [tester enterWorktime:10 Breaktime:4 Repetitions:5]; [tester tapViewWithAccessibilityLabel:@"Start Working"]; [tester waitForViewWithAccessibilityLabel:@"myTask"]; [tester waitForViewWithAccessibilityLabel:@"Start Working"]; } - (void)testPresetTask { [tester tapViewWithAccessibilityLabel:@"Presets"]; [tester tapRowAtIndexPath:@"Classic" inTableViewWithAccessibilityIdentifier:@"Presets List"]; [tester tapViewWithAccessibilityLabel:@"Start Working"]; [tester waitForViewWithAccessibilityLabel:@"myTask"]; [tester waitForViewWithAccessibilityLabel:@"Start Working"]; } @end</code></pre> <p>上述代码中,我们看到许多封装函数。为保证用例结构清晰明朗,我们借鉴 selenium pageObject 的设计方式, 遵循如下规则:</p> <ul> <li>a. 将页面上的对元素的发现、操作处理抽象为相应的类,返回操作结果;</li> <li>b. 封装尽可能多的工具类;</li> <li>c. 测试用例只关注用例逻辑,步骤尽量简洁。</li> </ul> <p>如下图所示,在用例集 test suite 中,我们只保持清晰的用例逻辑;非用例逻辑的动作封装成相应地用例集的类 test suite additions ;因为 KIF 的开源性,我们还可以利用 KIF 的私有 API 封装我们需要的工具 Tools 类。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/815bc1837cc2c141fdbfbfb3945f8791.png"></p> <p>(4)用例的运行独立和 retry 机制</p> <p>失败用例是不可避免的,上述用例的组织方式,降低了用例间的依赖性,但是并不能完全消除失败用例对后续用例执行的影响。如果能让每个用例独立启动 App 执行 case,则能保证后执行用例不受先执行失败用例的影响。如果在 case 运行失败后,还可以进行 retry 重试,则能提高用例运行的稳定性。xctool 工具能给我们带来这样的功能,我们用 xctool 命令先 build-tests 构建 app,然后循环启动 app 来 run-tests 用例,用例失败后,重新执行。下面是一个 xctool 独立运行用例的简单示例:</p> <pre> <code class="language-objectivec">xctool build-tests -workspace myApp.xcworkspace -scheme myKIFTestScheme -sdk iphonesimulator -configuration Debug -destination platform='iOS Simulator',OS=8.3,name='iPhone 6 Plus' array=( TimerTests HistoryTests ) for data in ${array[@]} do xctool -reporter pretty -reporter junit:tmp/test-report-tmp.xml -workspace myApp.xcworkspace -scheme myKIFTestScheme run-tests -only myKIFTestTarget:${data} -sdk iphonesimulator -configuration Debug -destination platform='iOS Simulator',OS=8.3,name='iPhone 6 Plus' done</code></pre> <h2>三、KIF 自动化的持续集成</h2> <h3>1. 持续集成的意义与 UI 自动化测试的用例选择</h3> <p>持续集成是一个自动化的周期性的集成测试过程,从检出代码、编译构建、运行测试、结果记录、测试统计等都是自动完成的,无需人工干预。我们的项目都是团队协作开发,采用持续集成的优势显而易见:</p> <ul> <li>尽早尽快地发现集成错误,保证团队开发人员提交代码的质量,减轻软件发布时的压力;</li> <li>自动完成集成中的环节,有利于减少集成过程的重复工作以节省时间、费用和工作量;</li> </ul> <p>持续集成最大的好处在于能够尽早高效发现问题,降低解决问题的成本。而发现问题的手段主要就是测试。</p> <p>根据 Martin Fowler 的测试理论,测试应该遵循如下测试金字塔组合,测试金字塔最底层是单元测试,然后是集成测试,继而是面向应用程序服务层的中间层测试,最高层是面向用户的业务逻辑测试:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/267c744022bab0850b283adef6a65bbf.png"></p> <p>测试自动化的测试层级越多,持续集成平台就能产生越大的价值。</p> <p>UI 测试目标是覆盖最核心的代码,尽可能去掉依赖,让不稳定因子降到最低,这样既保证自动化测试层级的全面性,又保证持续集成的稳定构建,降低测试的投入产出比。因此,在我们的 UI 自动化测试中,我们选择核心功能的冒烟用例来完成持续集成中的测试金字塔。</p> <h3>2. Jenkins 上完成基于 KIF 的 UI 自动化持续集成搭建</h3> <p>Jenkins 是一个开源的持续集成工具,提供了一种易于使用的持续集成系统,使开发者从繁杂的集成中解脱出来,专注于更为重要的业务逻辑实现上。</p> <p>Jenkins 以 Job 为单位运行项目,一个 Job 的工作流程为:在指定的时机,选择合适的 salve 节点,从版本管理系统上获取对应的源码,使用命令行脚本或者 maven 或者 ant 进行构建,构建后归档文件,处理报告,如果构建失败那么就通过邮件进行反馈等。</p> <p>Job 的触发时机主要有3种选择:</p> <ul> <li>"Build after other project are build":表示在其他某个项目build后触发,比如我们可以在某个提测Job构建之后,立即构建我们的 UI 自动化来验证这个提测的可行性;</li> <li>"Build periodically":表示按时间触发,我们可以选择这个让 Job 做 Daily Build 来进行持续构建观察;</li> <li>"Poll SCM":表示允许用户让 Jenkins 定期查询某一个项目的代码库,如果有代码变动则触发执行任务,这种触发非常适合集成测试项目,以此验证代码库变动是否能测试通过。</li> </ul> <p>我们希望在代码改动发生的时候就做到尽早发现代码改动带来的问题,所以使用 “Poll SCM” 在当代码仓库有新的 pull request 的时候触发相应 Job 完成构建,Job 的执行结果作为这个 pull request 能否合入的衡量指标之一;同时为支持客户端支持 daily build ,Job 使用 "Build periodically" 在每天 daily build 打包前完成一次自动构建。</p> <p>Job 需要支持命令行构建才能实现持续集成,如上一部分提到,我们可以借助 xcodebuild/xctool 实现单命令行构建。同时为了衡量 Job 的执行结果,我们需要在 Job 执行完成后生成相应的测试报告和代码覆盖率报告,使用 xcodebuild/xctool 这样的命令行工具,只需要配置相关的参数即可获取相应的 XML 测试报告文件。</p> <p>Jenkins 中 JUnit Plugin 插件可以将 XML 形式的测试报告转化成一种随时间推移的测试结果图表,向我们展示测试的结果和测试的稳定性; Cobertura plugin 插件可以将 XML 形式的覆盖率文件转化成一种随时间推移的代码覆盖率图表。如下图是 Job 中测试报告的代码覆盖率和测试结果的示例,通过下面的图表,我们可以清晰地看到测试是否通过,检查代码的测试覆盖范围,并对比历史的测试结果和代码覆盖率来推断和定位问题。</p> <p><img src="https://simg.open-open.com/show/07a39c0f23c25c9627e52d3173537df5.png"></p> <h3>3. KIF 自动化测试在 Jenkins 持续集成过程中遇到的问题</h3> <p>(1)设备重置</p> <p>我们的测试用例覆盖了第一次安装启动的操作。在初期,这个用例经常失败。经过排查发现,持续集成系统中的模拟器设备重置操作并没有覆盖所有的设备,UI 测试 Job 运行时,Job 选择的模拟器设备上可能遗留了其他 Job 构建的相同的 app 产物,导致我们的 Job 构建产物并不是第一次安装启动。所以在脚本中我们遍历所有模拟器设备,将其进行重置。</p> <p>(2)键盘敲击延迟</p> <p>我们的测试用例在输入框输入文字时,经常出现输入不全而导致失败的问题。比如在输入框中输入 'beijing' ,失败后提示:Failed to get text in field; instead, it was 'beiji' 。经过排查,发现持续集成系统中的机器性能有高有低,在低性能机器中更容易发生此问题,再研究 KIF 框架源码发现,KIF 默认设置的键盘敲击时延为一个常数,对于低性能机器来说这个敲击时延较短,容易漏掉输入,所以我们在 KIFTypist.m 源码文件中适当增加 (NSTimeInterval) keystrokeDelay 的时长来避免输入不全的问题。</p> <p>(3)多个系统弹窗确认</p> <p>前面我们提到过,KIF 支持对系统弹窗的处理,即接口 acknowledgeSystemAlert ,它能帮我们确认一个系统弹窗。但是我们的应用程序在启动时系统弹窗并不止一个,并且在不同设备上,因系统设置不同,系统弹窗的个数是不确定的。所以,直接使用 acknowledgeSystemAlert 并不能帮我们解决问题。因为 KIF 的开源性,我们在 KIF 框架源码 acknowledgeSystemAlert 函数中做了一次 while 循环处理,处理了出现的任意多个系统弹窗的情况,从而解决了问题。</p> <h2>参考文献:</h2> <ol> <li>Automate UI Testing in iOS: <a href="/misc/goto?guid=4959713918220194007" rel="nofollow,noindex">https://developer.apple.com/library/tvos/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/UIAutomation.html</a></li> <li>Appium 官网介绍: <a href="/misc/goto?guid=4959713918326416944" rel="nofollow,noindex">http://appium.io/slate/cn/v1.2.0/?ruby#appium</a></li> <li>Frank 官网介绍: <a href="/misc/goto?guid=4958543038295981736" rel="nofollow,noindex">http://www.testingwithfrank.com/</a></li> <li>KIF 源码库: <a href="/misc/goto?guid=4958543037995489543" rel="nofollow,noindex">https://github.com/kif-framework/KIF</a></li> <li>iOS UI Testing with KIF: <a href="/misc/goto?guid=4959713918470288534" rel="nofollow,noindex">http://www.raywenderlich.com/61419/ios-ui-testing-with-kif</a></li> <li>The current state of iOS automated functional testing: <a href="/misc/goto?guid=4959713918558109431" rel="nofollow,noindex">http://watirmelon.com/2013/11/04/the-current-state-of-ios-automated-functional-testing/</a></li> <li>Page Object: <a href="/misc/goto?guid=4959713918649759753" rel="nofollow,noindex">http://martinfowler.com/bliki/PageObject.html</a></li> <li>Test Pyramid: <a href="/misc/goto?guid=4958969854140119347" rel="nofollow,noindex">http://martinfowler.com/bliki/TestPyramid.html</a></li> <li>Continuous Integration: <a href="/misc/goto?guid=4958976758720450178" rel="nofollow,noindex">http://www.martinfowler.com/articles/continuousIntegration.html</a></li> <li>xcodebuild: <a href="/misc/goto?guid=4959647028392484141" rel="nofollow,noindex">https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/xcodebuild.1.html</a></li> <li>xctool: <a href="/misc/goto?guid=4958822653748995612" rel="nofollow,noindex">https://github.com/非死book/xctool</a></li> <li>Jenkins 官网介绍: <a href="/misc/goto?guid=4959647999236775758" rel="nofollow,noindex">https://wiki.jenkins-ci.org/display/JENKINS/Home</a></li> <li>JUnit Plugin: <a href="/misc/goto?guid=4959713918889839061" rel="nofollow,noindex">https://wiki.jenkins-ci.org/display/JENKINS/JUnit+Plugin</a></li> <li>Cobertura plugin: <a href="/misc/goto?guid=4959713918986127284" rel="nofollow,noindex">https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin</a></li> <li>Xcode 7 UI Testing: <a href="/misc/goto?guid=4959713919092567881" rel="nofollow,noindex">https://developer.apple.com/videos/play/wwdc2015/406/</a></li> </ol> <p> </p> <p>来自:http://tech.meituan.com/iOS-UITest-KIF.html</p> <p> </p>