UIKit 集成测试库。通过 UIKit 便捷地模拟高级用户交互。
与 KIF 不同,Robot 旨在不是完美地模拟用户如何与系统交互。相反,它试图复制相同的行为,同时最大限度地减少基于时间的操作的开销。一个完美的例子是禁用动画以加速测试的运行。
与 KIF 一样,Robot 也不是一个完整的集成测试解决方案。它更倾向于依赖其他测试框架来进行断言和运行。除了 XCTest 之外,还有一些流行的行为驱动测试框架:
同样,它也使用私有 API。
// tap on a cancel button/label
tapOn(theFirstView(withLabel(@"Cancel")));
使用 Cocoapods
pod 'Robot'
或者将其作为子项目导入并链接 IOKit
。最简单的方法是在您的 "其他链接器标志" 构建设置中添加它。
-framework IOKit
此库包括四个主要部分:
此组件提供了一种用于查找视图的 DSL。它们是基于递归子视图遍历和 NSPredicate 构建。
以下是一些查找视图的核心函数:
RBViewQuery *allViews(NSPredicate *predicate)
- 返回所有(包括子视图)满足给定范围中谓词的视图。默认范围为 keyWindow。RBViewQuery *allSubviews(NSPredicate *predicate)
- 返回所有满足给定范围中谓词的子视图。默认范围为 keyWindow。RBViewQuery *theFirstView(NSPredicate *predicate)
- 返回给定范围中满足谓词的第一个视图(或子视图)。默认范围为 keyWindow。RBViewQuery *theFirstSubview(NSPredicate *predicate)
- 返回给定范围中满足谓词的第一个子视图。默认范围为 keyWindow。所有核心方法都接受一个谓词来检查每个视图是否满足要求。您可以从 NSPredicate 创建自己的,但 Robot 还提供了一些内置的谓词以便组合使用。
where(NSString *formatString, ...)
是对 +[NSPredicate predicateWithFormat:predicateFormat, ...]
的一个别名。同样地,NSPredicate *where(BOOL(^)(UIView *view))
是对 +[NSPredicate predicateWithBlock:matcher]
的一个别名
// finds all views that have more than 2 subviews
allViews(where(@"subviews[SIZE] > %@", @2));
// finds all views that have a tag of 3
allViews(where(^BOOL(UIView *view){
return view.tag == 3;
}));
类似地,matching(...)
宏是对 +[NSCompoundPredicate andPredicateWithSubpredicates:@[...]]
的一个别名
// find all views with tag of 3 with more than 2 subviews.
allViews(matching(where(@"subviews[SIZE] > 2"), where(@"tag == 3")));
可以通过视图类对视图进行过滤的方法
// find all UITextViews, but not subclasses
allViews(ofExactClass([UITextView class]));
allViews(ofExactClass(@"UITextView"));
// find all UIButtons and subclasses
allViews(ofClass([UIButton class]));
allViews(ofClass(@"UIButton"));
您也可以使用另一个谓词过滤父视图
// find all views that have UIViews as superviews
allViews(withParent(ofExactClass([UIView class])));
// find all views that have superviews that have UIView classes. This includes
// the root view.
allViews(includingSuperViews(ofExactClass([UIView class])));
// all views, excluding the root view
allViews(withoutRootView());
或者通过视图内容
// find any views with the text of "Cancel"
allViews(withText(@"Cancel"));
// find any views with the text or accessibilityLabel of "Cancel"
allViews(withLabel(@"Cancel"));
// find any views with the EXACT image
allViews(withImage([UIImage imageNamed:@"myImage"]));
// find any views behaviorally acts like a button
allViews(withTraits(UIAccessibilityTraitButton));
// find any views that are accessible
allViews(withAccessibility(YES));
最后,通过可见性
// find all views that are visible (isHidden = NO and alpha > 0 and a drawable pixel)
// a drawable pixel is where clipsToBounds is NO or a non-zero size
allViews(withVisibility(YES));
// find all views that are on screen. On screen means the view's rect intersects or is
// inside the window. If not in a window, the root view is used instead.
allViews(onScreen(YES));
// find all views that are visible and on screen -- including all their superviews.
// This is a combination of withVisibility() and onScreen() with includingSuperViews().
allViews(onScreenAndVisible(YES));
所有核心查询方法都返回 RBViewQuery
,它是视图的懒加载 NSArray。它们可以通过属性块进一步细化。例如,要将查询限制为特定视图
// returns views with text "hello" that are either myView or any of its subviews
allViews(withText(@"Hello")).inside(myView);
// if you want to search inside multiple disperate view hierarchies
allViews(withText(@"Hello")).insideOneOf(@[myView1, myView2]);
也可以使用 NSSortDescriptors
数组来应用排序
// sort all the views by smallest origin first. Smallest is by y first, then x.
allViews(...).sortedBy(@[smallestOrigin()]);
// reverse sort
allViews(...).sortedBy(@[largestOrigin()]);
// sort all the views by smallest size first. Smallest is by height first, then width.
allViews(...).sortedBy(@[smallestSize()]);
// reverse sort
allViews(...).sortedBy(@[largestSize()]);
所有这些都可以链式调用
allViews(...).inside(myView).sortedBy(@[smallestOrigin()]);
如果没有模型来检查滚动之外的内容,那么检查表格视图的行为仍然很麻烦。请使用 RBTableViewCellsProxy
RBTableViewCellsProxy *cells = [RBTableViewCellsProxy cellsFromTableView:tableView];
cells[0] // -> Returns proxy to the first table view's cell
[cells[0] textLabel].text // works as expected
cells[100] // -> Returns another proxy
[cells[100] textLabel].text // works. Table view is scrolled before accessed.
Robot 利用基本接口封装了 UIKit 的键盘,以控制它。该接口位于 RBKeyboard
// focus a text field to get keyboard focus
tapOn(textField);
// type through the keyboard
[[RBKeyboard mainKeyboard] typeString:@"Hello World!"];
// dismiss the keyboard - you must always do this otherwise the next
// time you use the keyboard it might crash.
[[RBKeyboard mainKeyboard] dismiss];
要在键盘上键入特殊字符,请使用 -[typeKey:]
取代
// press delete key
[[RBKeyboard mainKeyboard] typeKey:RBKeyDelete];
Robot 实现了自己的 UITouch 子类,RBTouch
,通过您的应用程序来模拟触摸事件。您可以使用此类来模拟任何复杂的触摸交互。
除了 RBTouch
之外,还有 DSL 函数,它可以保持测试中的语法简洁。
最常见的操作是点击元素
tapOn(myButton);
tapOn(myViewQuery);
但它支持更复杂的手势
swipeLeftOn(myView);
swipeUpOn(myView);
swipeDownOn(myView);
swipeRightOn(myView);
根据需要,Robot 可以选择性地加快特定操作。要禁用测试时的动画并调用任何完成块,请使用 -[disableAnimationsInBlock:]
API
[RBTimeLapse disableAnimationsInBlock:^{
[UIView animateWithDuration:2 animations:^{
view.x = 200;
} completion:^(BOOL finished){
view.hidden = YES;
}];
}];
view.isHidden // => YES;
内部,RBTimeLapse
将在禁用动画的同时推进运行循环,并将计时器延迟设置为零。
如果您只想要后者而不禁用动画,可以这样做
[logger performSelector:@selector(logMessage:) withObject:@"hello" afterDelay:1];
[RBTimeLapse advanceMainRunLoop]; // calls [logger logMessage:@"hello"]
对于 tapOn
,时间流逝是自动的,但对于任何其他手势则不是