API
EarlGrey包含三组主要的API:
- 交互API
- 同步API
- 其他API
交互API
EarlGrey的测试用例由一系列的UI元素交互组成。 每个交互包括:
- 选择一个元素进行交互
- 对其执行操作,与(&&) /或(||)
- 做出断言来验证元素的状态和行为
为了体现上述行为,交互API由以下内容组成:
- 选中元素相关API
- 操作相关API
- 断言相关API
这些API中的每一个接口都考虑到了可扩展性,让用户可以进行灵活的定制,同时保留了测试代码的可读性。阅读下面的代码片段:
[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")];
它显示了如何通过Accessibility Identifier为ClickMe的元素来启动交互。选择元素后,你可以以操作该元素或者断言的方式继续交互。 EarlGrey可以使用任何符合UIAccessibility protocol的元素,而不仅仅是UIViews,这就让测试用例可以执行更丰富的交互。
你可以将元素选择和操作在一行代码中完成。例如,点击一个Accessibility Identity为ClickMe的元素可以这样写:
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
performAction:grey_tap()];
注意:selectElementWithMatcher:不返回一个元素, 它只是标志着互动的开始。
你可以将元素选择和断言在一行代码中完成。下面的代码将展示选择一个Accessibility Identity为ClickMe的元素并判断它是否显示:
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
assertWithMatcher:grey_sufficientlyVisible()];
最后,你可以按顺序执行操作和断言。 下面的代码展示了查找Accessibility Identity为ClickMe的元素,点击它并判断其是否显示。
[[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
performAction:grey_tap()]
assertWithMatcher:grey_notVisible()];
以下是一个简单的交互模板:
[[[EarlGrey selectElementWithMatcher:<element_matcher>]
performAction:<action>]
assertWithMatcher:<assertion_matcher>];
其中<element_matcher>和<assertion_matcher>是包含在GREYMatchers.h中的匹配器,<action>为GREYAction.h中的操作方法。因为每个匹配器或操作都有一个简写的方法,所以可以使用grey_sufficientlyVisible(),来替代[GREYMatchers matcherForSufficientlyVisible]。用简写方法的方式默认是允许的。 要禁用它,请将#define GREY_DISABLE_SHORTHAND 1添加到项目的头文件。
选中元素API
使用Selection API来定位屏幕中的UI元素。当API接收到Matcher后会在所有UI分层中定位对应的元素来发生交互。在EarlGrey中,matchers支持一种类似于OCHamcrest matchers的API。你可以使用与或逻辑组合matchers,允许你创建匹配规则,以确定UI分层中的任何元素。
EarlGrey Matchers
所有EarlGrey Matchers都可以在GREYMatchers工厂类中找到。 查找UI元素的最佳方法是使用它的accessibility properties。我们强烈建议使用accessibility identifier,因为它是一个元素的唯一标识。 使用grey_accessibilityID()作为matcher,通过其accessibility traits来定位UI元素。或者使用grey_accessibilityLabel()作为 accessibility labels的匹配器。
Matcher可能会匹配多个元素:例如,grey_sufficientlyVisible()将匹配所有可见的UI元素。 在这种情况下,你必须缩小选择范围,直到它可以唯一标识一个UI元素。 你可以通过将matchers与grey_allOf(),grey_anyOf(),grey_not()结合使用或通过向root matchers提供inRoot方法来缩小选择范围,从而使matchers更加准确。
来看下面的例子。
第一个,使用组合的matchers,以下这段代码将查找accessibility label为Send的元素并且显示在当前屏幕。
id<GREYMatcher> visibleSendButtonMatcher =
grey_allOf(grey_accessibilityLabel(@"Send"), grey_sufficientlyVisible(), nil);
[[EarlGrey selectElementWithMatcher:visibleSendButtonMatcher]
performAction:grey_tap()];
请注意,使用grey_allOf命令很重要。 如果先使用grey_sufficientlyVisible,则将检查整个应用程序中的每个元素的可见性。 从大多数选项中(比如accessibility label和accessibility id)中选择matchers至关重要。
接下来,使用inRoot,以下这段代码将查找accessibility label为Send且被包含在SendMessageView类实例化对象中的UI元素。
[[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"Send")]
inRoot:grey_kindOfClass([SendMessageView class])]
performAction:grey_tap()];
注意:为了与Swift兼容,我们分别使用grey_allOfMatchers()和grey_anyOfMatchers()来代替grey_allOf()和grey_anyOf()。
自定义 Matchers
要创建自定义matchers,请使用基于block的GREYElementMatcherBlock类。 例如,以下代码用来与没有子视图的视图匹配。
+ (id<GREYMatcher>)matcherForViewsWithoutSubviews {
MatchesBlock matches = ^BOOL(UIView *view) {
return view.subviews.count == 0;
};
DescribeToBlock describe = ^void(id<GREYDescription> description) {
[description appendText:@"Views without subviews"];
};
return [[GREYElementMatcherBlock alloc] initWithMatchesBlock:matches
descriptionBlock:describe];
}
MatchesBlock除了接受UIView *之外,也可以接受id类型。当matcher用于选取元素时,需要使用id类型。以下matcher示例通用于所有UI元素类型:
+ (id<GREYMatcher>)matcherForElementWithoutChildren {
MatchesBlock matches = ^BOOL(id element) {
if ([element isKindOfClass:[UIView class]]) {
return ((UIView *)element).subviews.count == 0;
}
// Handle accessibility elements here.
return ...;
};
DescribeToBlock describe = ^void(id<GREYDescription> description) {
[description appendText:@"UI element without children"];
};
return [[GREYElementMatcherBlock alloc] initWithMatchesBlock:matches
descriptionBlock:describe];
}
这个matcher可以在测试中用于选取没有任何子元素(或子视图)的元素并且双击它(假设该方法是在CustomMatchers类中声明的):
[[EarlGrey selectElementWithMatcher:[CustomMatchers matcherForElementWithoutChildren]]
performAction:grey_doubleTap()];
选取未显示的UI元素
在某些情况下,UI元素可能被隐藏或在屏幕外,需要某些交互才能将其展示在屏幕上。 常见示例包括滚动视图中元素未被滚动至可见,放大地图至街景模式可见的UI元素,浏览自定义布局的UICollectionView等。
例如,以下代码通过aButtonMarcher匹配一个元素,此元素在aScrollViewMatcher匹配的滚动视图中,经过重复向下滚动(每次减少50points)找到该元素并点击
[[[EarlGrey selectElementWithMatcher:aButtonMatcher]
usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, 50)
onElementWithMatcher:aScrollViewMatcher]
performAction:grey_tap()];
以下代码用来定位一个UITableView视图中的指定Cell,aTableViewMatcher匹配了该表格视图,通过重复向下滚动(每次减少50points)找到aCellMatcher定位的指定Cell。
[[[EarlGrey selectElementWithMatcher:aCellMatcher]
usingSearchAction:grey_scrollInDirection(kGREYDirectionUp, 50)
onElementWithMatcher:aTableViewMatcher]
performAction:grey_tap()];
操作API
使用Action API来描述在选定UI元素上的测试操作。
EarlGrey Actions
所有EarlGrey actions都在GREYActions工厂类中。 最常见的操作是使用grey_tap()方法点击指定元素:
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"TapMe")]
performAction:grey_tap()];
如果某个操作触发UI层次结构中的更改,则EarlGrey会将每个操作(包括链式操作)与UI同步,以确保在执行下一个操作之前它处于稳定状态。
并非所有操作都可以在所有元素上执行,例如,无法对不可见的元素执行点按操作。为了确保操作可执行,每次执行之前要确保GREYMatcher定位到该元素。未满足这些条件会导致抛出异常并将测试标记为失败。 为了避免测试失败,可以调用performAction:error:获取包含错误详情的NSError对象。
NSError *error;
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"Non-Existent-Ax-Id")]
performAction:grey_tap()
error:&error];
在上面的例子中,在不存在的元素上执行EarlGrey交互时不会抛出异常。 相反,传递的错误对象将保存故障详细信息,并且不会立即使测试失败。 然后可以查看错误详细信息以查找故障详细信息。
自定义操作
自定义操作可以通过遵循GREYAction协议的方式来创建。为了方便起见,你可以使用已遵循GREYAction协议的GREYActionBlock,并允许你以block的形式自定义操作。 以下代码用block自定义一个操作(animateWindow)用来设置选中window的动画:
- (id<GREYAction>)animateWindowAction {
return [GREYActionBlock actionWithName:@"Animate Window"
constraints:nil
performBlock:^(id element, NSError *__strong *errorOrNil) {
// First, make sure the element is attached to a window.
if ([element window] == nil) {
// Populate error.
*errorOrNil = ...
// Indicates that the action failed.
return NO;
}
// Invoke a custom selector that animates the window of the element.
[element animateWindow];
// Indicates that the action was executed successfully.
return YES;
}];
}
断言API
使用断言API来验证UI元素的状态和行为。
使用Matchers断言
使用assertWithMatcher:用GREYMatcher执行断言。 所选元素将通过matcher判断其状态。例如,以下代码判断accessibility ID为ClickMe的元素是否可见,如果元素在屏幕上不可见,则测试失败。
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
assertWithMatcher:grey_sufficientlyVisible()];
为了防止测试失败,可以为assertWithMatcher:error:方法提供一个NSError对象,如下面的代码所示。 相比直接测试失败,它将为你提供一个NSError对象,其中包含有关失败的详细信息。
NSError *error;
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
assertWithMatcher:grey_sufficientlyVisible()
error:&error];
你也可以使用assert方法传递一个GREYAssertion实例来执行断言。我们建议你尽可能使用assertWithMatcher:创建断言。Matchers是轻量级的,并且适用于简单的断言。 然而,对于需要执行复杂逻辑的断言(如操作UI)再继续断言,自定义GREYAssertion可能是更好的选择。
自定义断言
你可以使用GREYAssertion协议或GREYAssertionBlock来创建自定义断言。以下代码使用GREYAssertionBlock编写断言来检查视图的alpha值是否等于预期的值:
+ (id<GREYAssertion>)hasAlpha:(CGFloat)alpha {
return [GREYAssertionBlock assertionWithName:@"Has Alpha"
assertionBlock:^(UIView *view, NSError *__strong *errorOrNil) {
if (view.alpha != alpha) {
NSString *reason =
[NSString stringWithFormat:@"Alpha value doesn't match for %@", view];
// Check if errorOrNil was provided, if so populate it with relevant details.
if (errorOrNil) {
*errorOrNil = ...
}
// Indicates assertion failed.
return NO;
}
// Indicates assertion passed.
return YES;
}];
}
注意:不要认为断言是针对有效的UI元素运行的。 在验证断言之前,你需要自己检查UI元素以确保UI元素处于有效状态。 例如,下面的代码就是在执行断言之前判断元素是否存在:
+ (id<GREYAssertion>)hasAlpha:(CGFloat)alpha {
return [GREYAssertionBlock assertionWithName:@"Has Alpha"
assertionBlock:^(UIView *view, NSError *__strong *errorOrNil) {
// Assertions can be performed on nil elements. Make sure view isn’t nil.
if (view == nil) {
// Check if errorOrNil was provided, if so populate it with relevant details.
if (errorOrNil) {
*errorOrNil = [NSError errorWithDomain:kGREYInteractionErrorDomain
code:kGREYInteractionElementNotFoundErrorCode
userInfo:nil];
}
return NO;
}
// Perform rest of the assertion logic.
...
// Indicates assertion passed.
return YES;
}];
}
或者,你可以创建一个和GREYAssertion类相符的类。
错误处理器
默认情况下,在发生异常时,EarlGrey会使用错误处理器。 默认处理器会记录异常,截取屏幕截图,然后打印其路径以及其他有用的信息。你可以选择自定义错误处理器并替换掉全局默认的错误处理器,并使用EarlGrey setFailureHandler:安装在API上。 要创建自定义错误处理器,请编写一个遵循GREYFailureHandler协议的类:
@interface@interface MyFailureHandler : NSObject <GREYFailureHandler>
@end
@implementation MyFailureHandler
- (void)handleException:(GREYFrameworkException *)exception details:(NSString *)details {
// Log the failure and state of the app if required.
// Call thru to XCTFail() with an appropriate error message.
}
- (void)setInvocationFile:(NSString *)fileName
andInvocationLine:(NSUInteger)lineNumber {
// Record the file name and line number of the statement which was executing before the
// failure occurred.
}
@end
断言宏
EarlGrey提供了自带的宏,可以在测试用例中用于断言和验证。 这些宏与XCTest提供的宏相似,档断言失败时调用全局的错误处理器。
GREYAssert(expression, reason, ...)— 如果表达式输出结果为false,则失败GREYAssertTrue(expression, reason, ...)— 如果表达式输出结果为false,则失败。用于BOOL表达式GREYAssertFalse(expression, reason, ...)— 如果表达式输出结果为true,则失败。用于BOOL表达式GREYAssertNotNil(expression, reason, ...)— 如果表达式输出结果为nil,则失败。GREYAssertNil(expression, reason, ...)— 如果表达式输出结果非nil,则失败。GREYAssertEqual(left, right, reason, ...)— 如果 left != right时失败GREYAssertNotEqual(left, right, reason, ...)— 如果 left == right时失败GREYAssertEqualObjects(left, right, reason, ...)— 如果 [left isEqual:right]返回false时失败GREYAssertNotEqualObjects(left, right, reason, ...)— 如果 [left isEqual:right]返回true时失败GREYFail(reason, ...)— 失败后提供失败的原因GREYFailWithDetails(reason, details, ...)— 失败后提供失败的原因和细节
布局测试
EarlGrey提供API来验证UI元素的布局,例如验证元素X位于元素Y的左侧。布局断言是根据NSLayoutConstraint构建的。要验证布局,需要先创建一个约束来描述布局,选择一个约束它的元素,然后使用grey_layout(…)声明它与约束匹配。 请注意,grey_layout(…)接受的所有约束都必须满足。 这可以简单地描述复杂的布局断言。
例如,以下代码就是指定元素位于所有其他元素右边。
GREYLayoutConstraint *rightConstraint =
[GREYLayoutConstraint layoutConstraintWithAttribute:kGREYLayoutAttributeLeft
relatedBy:kGREYLayoutRelationGreaterThanOrEqual
toReferenceAttribute:kGREYLayoutAttributeRight
multiplier:1.0
constant:0.0];
你现在可以选择accessibility ID为RelativeRight的元素,并使用grey_layout(@ [rightConstraint])来断言它是否位于accessibility ID为TheReference元素的右侧。
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@”RelativeRight”)]
assertWithMatcher:grey_layout(@[rightConstraint], grey_accessibilityID(@”TheReference”))];
你还可以使用layoutConstraintForDirection:创建简单的定向约束。 以下代码效果等同于前面的示例:
GREYLayoutConstraint *rightConstraint =
[GREYLayoutConstraint layoutConstraintForDirection:kGREYLayoutDirectionRight
andMinimumSeparation:0.0];
同步API
这些API可让你控制EarlGrey与待测应用程序同步。
GREYCondition
如果你的测试用例需要特殊处理,你可以使用GREYCondition等待或同步某些条件。 GREYCondition采用返回BOOL的block来表示是否满足条件。 在继续测试剩余部分之前,框架会轮询该block直到满足条件。 以下代码说明如何创建和使用GREYCondition:
GREYCondition *myCondition = [GREYCondition conditionWithName:@"my condition"
block:^BOOL {
... do your condition check here ...
return yesIfMyConditionWasSatisfied;
}];
// Wait for my condition to be satisfied or timeout after 5 seconds.
BOOL success = [myCondition waitWithTimeout:5];
if (!success) {
// Handle condition timeout.
}
同步性
EarlGrey通过跟踪主要调度队列,主操作队列,网络,UI绘制和其他几个信号自动等待应用程序闲置,并仅在应用程序处于空闲状态时才执行交互。 但是,尽管有时应用程序处于忙碌状态,但仍可能会出现有些交互必须执行的情况。 例如,在消息类的应用程序中,照片可能正在上传并且相应UI绘制正在运行,此时仍然需要测试输入并发送文字信息。在这种情况下,可以使用kGREYConfigKeySynchronizationEnabled禁用EarlGrey的同步,如以下代码所示:
[[GREYConfiguration sharedInstance] setValue:@(NO)
forConfigKey:kGREYConfigKeySynchronizationEnabled];
一旦禁用,所有的交互都将继续进行,无需等待应用程序闲置,直到重新启用同步。 请注意,为了最大限度地提高测试效率,必须尽快重新启用同步。 而不是完全禁用同步,你可以配置同步参数以适应你的应用程序的需求。 例如:
kGREYConfigKeyNSTimerMaxTrackableInterval用来设定非重复NSTimers的最大间隔。kGREYConfigKeyDispatchAfterMaxTrackableDelay通过dispatch_after调用设定下一步操作的最大延迟时间。kGREYConfigKeyURLBlacklistRegex用来设定不需要同步的URL黑名单的正则表达式
在GREYConfiguration中有几种类似的配置。 详细请参阅下面的一些特定用例。
网络
默认情况下,EarlGrey会与所有网络请求同步,但你可以通过正则表达式来自定义无需同步的请求。如果要将一个URL列入黑名单中,请创建一个与其匹配的正则表达式,将其添加到NSArray并将其传递给GREYConfiguration。 对于多个网址,请通过为每个网址创建一个正则表达式来重复上述的过程。例如,要告诉框架不要同步www.google.com和www.youtube.com,请执行以下操作:
NSArray *blacklist = @[ @".*www\\.google\\.com", @".*www\\.youtube\\.com" ];
[[GREYConfiguration sharedInstance] setValue:blacklist
forConfigKey:kGREYConfigKeyURLBlacklistRegex];
交互超时
无限重复的页面绘制或长时间运行的动画会影响同步。 绘制时间超过测试时间最大值的动画会导致超时异常。 为了避免这种情况,EarlGrey将绘制持续时间限制为5秒,并禁用重复绘制(连续绘制只运行一次)。要操作EarlGrey允许绘制运行更长时间,请更改允许的最大绘制持续时间值。 确保这不会导致你的测试超时。 以下代码将最大绘制持续时间增加到30秒。
[[GREYConfiguration sharedInstance] setValue:@(30.0)
forConfigKey:kGREYConfigKeyCALayerMaxAnimationDuration];
非主线程调用
由于分发队列的限制和EarlGrey与它们同步的方式,从dispatch_queue调用EarlGrey语句会导致活锁。 为了减少这种情况,我们引入了新的基于block的API来封装EarlGrey语句,并且可以从非主线程安全地调用它们:
grey_execute_sync(void (^block)())— 同步。 阻塞直到执行完成。grey_execute_async(void (^block)())— 异步。
其他API
UI交互之外,你可以操作EarlGrey以多种方式控制设备和系统。
全局配置
GREYConfiguration类允许你配置框架。 它提供了配置同步,交互超时,操作约束checks,日志记录等方法。只要配置发生更改,它就会全局应用。 例如,以下代码将启用详细日志:
[[GREYConfiguration sharedInstance] setValue:@(YES)
forConfigKey:kGREYConfigKeyVerboseLogging];
控制设备的朝向
要旋转设备,请使用[EarlGrey rotateDeviceToOrientation:errorOrNil:]以指定设备朝向。例如,以下代码会操作你的系统(和你的应用)从竖屏变为横屏并且Home键位于右侧:
[EarlGrey rotateDeviceToOrientation:UIDeviceOrientationLandscapeLeft errorOrNil:nil];
你可以从以下模式中进行选择(有关更多信息,请参阅UIDeviceOrientation):
UIDeviceOrientationUnknownUIDeviceOrientationPortraitUIDeviceOrientationPortraitUpsideDownUIDeviceOrientationLandscapeLeftUIDeviceOrientationLandscapeRightUIDeviceOrientationFaceUpUIDeviceOrientationFaceDown
摇一摇操作
你可以使用[EarlGrey shakeDeviceWithError:]模拟模拟器中的摇一摇操作。