KIF-- iOS UI 自动化测试探索
在我们探索自动化测试之前,我们先了解一下自动化测试的优缺点和还有,什么样的业务适合自动化测试。
自动化测试
自动化测试就是写一些测试代码,用代码代替人工去完成模块和业务的测试。
其实不管是开发还是测试,如果你在不断的做重复性工作的时候,就应该问自己一个问题:是不是有更高效的办法?
-
自动化测试的优点:
- 测试速度快,避免重复性工作
- 避免regression(回退),让开发更有信心去修改,优化甚至重构代码。(有测试代码做依托,不怕业务逻辑丢失,混乱)
- 测试结果一致性
- 自动化测试的实现,有助于持续集成的可行性和可靠性提升
- 强化开发人员编写高质量代码(自动化测试,不通过,不能提交合并)
-
自动化测试的一些缺点:
- 开发和维护成本高,需要专业的测试人员
- 不能完全替代人工测试
- 本身的测试代码的准确性,还有无法保证测试的准确性-让代码去判断一段逻辑是否正确还是可行的,但是要判断一个控件是否显示正确,代码很难实现
- 团队的建设和方案的选取等一系列的问题
所以,在做自动化测试之前,我们首先要针对项目提出几个问题:
- 这个测试业务的变动是否频繁
- 这个测试业务是否属于核心功能
- 编写测试代码的成本是多少,是否划算
- 自动化测试能保证测试结果的准确么
通常我们只会选择那些业务稳定,需要频繁测试的部分来编写自动化脚本,其余的依然得人工测试,人工测试是 iOS App 开发中不可缺少的一部分。
测试种类
从是否接触源代码的角度来分类: 测试分为黑盒和白盒。
白盒测试的时候,测试人员是可以直接接触待测试App的源代码的。白盒测试更多的是单元测试,测试人员针对各个单元进行各种可能的输入分析,然后测试其输出。白盒测试的测试代码通常由iOS开发编写。
黑盒测试。黑盒测试的时候,测试人员不需要接触源代码。是从App层面对其行为以及UI的正确性进行验证,黑盒测试由iOS测试完成。
从业务层次来说 iOS 测试通常只有以下两个层次:
Unit,单元测试,保证每一个类都能够正常工作
UI,UI 测试,也叫做集成测试,从业务层的角度保证各个业务可以正常工作。
框架选择
测试框架五花八门,一定要选择适合自己团队的,测试效率,集成难易度,维护难易度等等,选择框架的时候我们要考虑一下几个方面:
- 测试代码编写的成本
- 是否可调式,调试是否便利
- 测试框架本身的稳定性
- 测试报告是否详细(截图,代码覆盖率...)
- WebView 的支持(H5混合的 App)
- 自定义控件的测试支持
- 是否需要源代码
- 是否需要连着电脑和设备
- 是否支持持续集成
其中单元测试,我们上一篇着重介绍了 BDD 的老牌测试框架 Kiwi ,就不多说了。
UI测试,UI 测试的框架有很多,有的是以 UI Automation 为基础,对其进行补充和优化,包括扩展型 UI Automation 和驱动型 UI Automation。
还有一些框架类型是私有 API 和注入编译型等。
在以上分类中挑选具有代表性的自动化框架:UI Automation、Appium、KIF、Frank、UI Testing 进行对比,下表是这几种测试框架的特点对比:
结合上面我们选择框架要考虑的几个方面,KIF框架已经展现了它的优势,并且KIF使用XCTest框架,使得其测试流程iOS程序的单测无异,可完全复用单测的持续集成流程,维护持续集成的成本相对降低;另外,KIF是一个活跃的开源测试框架,可扩展性好,升级更新快,有活跃社区来探讨和解决使用过程中遇到的问题。就是今天我们要介绍的重点,KIF。
KIF
KIF的全称是Keep it functional
。利用 Apple 给所有控件提供的辅助属性 accessibility attributes来定位和获取元素,完成界面的交互操作;结合使用 Xcode 的 XCTest 测试框架,拥有 XCTest 测试框架的特性,使得测试用例能以 command line build 工具运行并获取测试报告。
KIF 搭建
我们首先应该在工程项目中创建基于 Cocoa Touch Testing Bundle 模板的 Target ,并确保创建的 Target 的属性有如下设置:
“Build Phases”:设置Target Dependencies,UI自动化测试固然要依赖应用程序的App产物,所以需保证应用程序 Target 被添加在 Test Target 的 Target Dependencies 中。
“Build Settings”:设置 “Bundle loader”为:$(BUILT_PRODUCTS_DIR)/MyApp.app/MyApp;MyApp使你自己项目的路径
设置 “Test Host” 为:$(BUILT_PRODUCTS_DIR);
设置 “Wrapper Extensions” 为:xctest。
- cocoaPods导入:
target 'Your Apps' do
...
end
target 'Acceptance Tests' do
pod 'KIF', :configurations => ['Debug']
end
- 手动导入,最新的
framework
方式导入,非常头疼- 下载KIF源码,选择
KIFFramework
这个scheme
编译,products 里面生成KIF.framework
,show in finder 把它拷贝到我们需要测试的项目里面去 - 打开 Xcode file new
Add target
选择 iOS Unit Testing Bundle 或者 iOS UI Testing bundle 设置一个自己喜欢的target 名称 - 选择我们新建的 target 点击
Build Phases
下的Link Binary With Libaries
,添加我们刚拷贝过来的KIF.framework
,系统依赖库QuartzCore.framework
、CoreGraphics.framework
- 然后选择这个 target 的
Build Settings
,在Other Linker Flags
里面添加-framework IOKit
和-ObjC
这两选项 - 接着设置
User Header Search Paths
和Framework Search Paths
的路径为我们新建的 target - 最后,设置
Bundle Loader
为"$(BUILT_PRODUCTS_DIR)/MyApplication.app/MyApplication
" 里面的MyApplication
是自己自己项目的名字 - 最后一步,最重要的一部,先把项目跑一遍,生成
MyApplication.app
之后再执行command + U
开始 testing
- 下载KIF源码,选择
- 现在可以开始写我们的测试用例了
开始之前,我想来张图,KIF 基于苹果给所有控件添加的一个accessibility 属性来实现的,所以在 Storyboard 上我们有两种方式设置
还可以通过代码设置:
[alert setAccessibilityLabel:@"Label"];
[alert setAccessibilityValue:@"Value"];
[alert setAccessibilityTraits:UIAccessibilityTraitButton];
为了跟原业务代码隔离,我们在业务代码中应该建立宏来隔离我们设置 accessibility 属性的代码,如下面的例子:
#ifdef DEBUG
[tableView setAccessibilityValue:@"Main List Table"];
#endif
#ifdef KIF_TARGET (这个值需要在build settings里设置)
[tableView setAccessibilityValue:@"Main List Table"];
#endif
测试用例的编写和组织
- accessibility属性设置
accessibility 属性是Apple给视觉障碍人群提供完全无障碍使用的基本属性,该属性表明了UI元素的可访问性、是什么、做什么以及会触发什么样的操作。原生的UIKit控件默认提供了这些信息,然而,自定义的控件则需要对该属性进行设置,设置方式可参考下面几点:
- 设置方式:storyboard 设置,代码设置
- 查看方式:Xcode打开
Open Developer Tool
开启模拟器的 Accessibility Inspector功能,即可看到控件的 accessibility 属性。 - 设置建议:设置的 AccessibilityLabel 属性值要有实际意义(用户可理解)因为设置这个属性后用户可以通过 VoiceOver访问;用户不可访问的控件,比如某些放置控件的容器等应该设置为 AccessibilityIdentifier 。
- KIF 常用操作接口(
KIFUITestActor.h
里可查阅)
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;
扩展:我们还可以对 KIFUITestActor 类进行扩展,利用 KIFUITestActor 中的私有函数,使 AccessibilityIdentifier 代替 Label 识别元素,完成 tapThisView 、waitForView 等操作。
- KIF测试用例集操作(KIFTestCase.h 中可查阅)
/*!
* @abstract This method runs once before executing the first test in the class.
* @discussion This should be used for navigating to the starting point in the app where all tests will start from. Because this method is not guaranteed to run in the same instance as tests, it should not be used for setting up instance variables but can be used for setting up static variables.
*/
/*
在本类中第一个 test case 执行前执行一次,用来执行本类中各个测试函数的公共操作
//注意:因为不能保证这个方法与 test case 是同一个类实例,所以不能用来设置实例变量的值,但是可以设置静态变量
*/
- (void)beforeAll;
/*!
* @abstract This method runs before each test.
* @discussion This should be used for any common tasks required before each test. Because this method is guaranteed to run in the same instance as tests, it can be used for setting up instance variables.
*/
//在每个具体 test case 执行前执行一次,用来执行各个函数需要的测试环境
//注意因为确保这个方法与 test case 是同一个类实例,可以用来设置实例变量
- (void)beforeEach;
/*!
* @abstract This method runs after each test.
* @discussion This should be used for restoring the app to the state it was in before the test. This could include conditional logic to recover from failed tests.
*/
//在每个具体 test case 执行完之后执行一次,用来清除状态,恢复至 test 之前的状态,可以包含一些条件判断逻辑,从失败的 test case 中恢复,以确保不影响之后的测试
- (void)afterEach;
/*!
* @abstract This method runs once after executing the last test in the class.
* @discussion This should be used for navigating back to the initial state of the app, where it was before @c beforeAll. This should also be used for tearing down any static methods created by @c beforeAll.
*/
//执行完本类的最后一个 test case 之后执行一次,用于将 App 恢复至测试的初始状
- (void)afterAll;
/*!
* @discussion When @c YES, rather than failing the test and advancing on the first failure, KIF will stop executing tests and begin spinning the run loop. This provides an opportunity for inspecting the state of the app when the failure occurred.
*/
- 系统的功能实现(
KIFSystemTestActor.h
中可查阅)
模拟用户旋转设备:- (void)simulateDeviceRotationToOrientation:(UIDeviceOrientation)orientation;
对当前屏幕截图并存储到硬盘中:- (void)captureScreenshotWithDescription:(NSString *)description;
用例组织
- 设计单个测试用例
- a.设置测试所需要的环境
- b.测试用例的具体测试逻辑
- c.恢复 App 至此次测试前的状态
a,c可用beforeEach
和alterEach
来实现,这样保证了每个用例之间的独立性和稳定性
一般来说,可将用例按功能分成若干个用例集,每个用例集按校验点或者功能点分成若干个用例,这样方便测试用例的管理和维护。某些含有耗费时间多,耗费资源多的公共操作的用例可以集合成一个用例集,在用例集运行前统一执行。
- 设计用例集
- 1.设置用例集需要的环境,公共操作
- 2.设计各个用例
- 3.恢复 App 至用例集测试的初始状态
1和3 步骤可以用beforeAll
和afterAll
来实现。下面简单展示一个用例集的书写:
#import "CrazyTests.h"
#import
#import
@implementation CrazyTests
- (void)beforeAll
{
[self setTestModel];
}
- (void)afterAll
{
[self resetTestModel];
[self cleanHistory];
}
- (void)beforeEach
{
[self setTestModel];
}
- (void)afterEach
{
[self cleanParams];
}
- (void)testNameTask
{
[tester enterText:_pp.nickName intoViewWithAccessibilityLabel:@"name"];
[tester enterText:_pp.realName intoViewWithAccessibilityLabel:@"password"];
[tester tapViewWithAccessibilityLabel:@"login"];
}
#pragma mark-- setting
- (void)setTestModel
{
_pp = [Person new];
_pp.age = 20;
_pp.nickName = @"crazy";
_pp.realName = @"hey";
_pp.cardId = @"123456789";
}
- (void)resetTestModel
{
_pp = nil;
}
- (void)cleanHistory
{
_pp = nil;
}
- (void)cleanParams
{
_pp = nil;
}
上述例子,只是简单说明。我们书写用例集应该遵循如下规则:
- 将页面上对元素的发现,操作处理抽象为相应的类,返回操作结果
- 封装尽可能多的工具类
- 测试用例只关注用例逻辑,步骤尽量简洁
我们可以利用 KIF 的私有 api 封装我们的工具类。
用例的独立运行和 retry 机制
失败用例是不可避免的,上述用例的组织方式,降低了用例间的依赖性,但是并不能完全消除失败用例对后续用例执行的影响。如果能让每个用例独立启动 App 执行 case,则能保证后面执行用例不受执行失败用例的影响。如果在 case 运行失败后,还可以进行 retry 重试,能提高用例运行的稳定性。xctool这个工具能给我们带来这样的功能,我们用 xctool 命令先 build-tests 构建 App,然后循环启动 App 来 run-tests用例,用例失败后,重新执行。下面是是一个 xctool 独立运行用例的简单示例:
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
一般我们测试,团队大了或者分工特别细的话,就需要接入自动化持续集成。后续大家可以自行了解,主要框架有Jenkins Fastlane等。
优化测试用例
当测试用例写多了,我们也会重构我们的测试用例代码。通常,我们应该从几个角度去考虑:
- 不要测试私有方法(封装是 OOP 的核心思想之一,不要为了测试破坏封装)
- 对测试用例分组(功能,业务相似)
- 对单个用例保证测试独立(不受之前测试的影响,不影响之后的测试),这是测试是否准确的核心
- 提取公共的代码和操作,减少 copy/paste这样的工作,测试用例是上层调用,只关心业务逻辑,不关心内部代码实现
总结
KIF 因为是建立在 XCTest
框架之上的,所以非常适合我们开发者上手,而且利用私有 API,我们可以很方便的测试 UI 层面和单元测试等
参考:
iOS自动化测试的那些干货
基于 KIF 的 iOS UI 自动化测试和持续集成
解放你的双手—iOS自动测试基础