iOS 测试 [Google EarlGrey] 0x02 API 简介

hillchan31 · 2016年03月01日 · 最后由 梦梦GO 回复于 2017年04月20日 · 2276 次阅读
本帖已被设为精华帖!

Google EarlGrey 学习笔记:
[Google EarlGrey] 0x00 安装及运行
[Google EarlGrey] 0x01 第一个测试用例
[Google EarlGrey] 0x02 API 简介

API

EarlGrey 主要包括三部分 APIs

  • 交互相关的编程接口 (Interaction APIs)

  • 同步相关的编程接口 (Synchronization APIs)

  • 其他的编程接口 (Other Top Level APIs)

交互相关的编程接口 (Interaction APIs)

EarlGrey 的测试用例由一系列的 UI 交互组成。每一步交互由以下步骤构成:

  • 选中需要交互的元素

  • 操作该元素

  • 断言操作后该元素的状态或者表现

针对以上三个步骤,交互相关的编程接口又可以分为:

  • 选中相关的编程接口 (Selection API)

  • 操作相关的编程接口 (Action API)

  • 断言相关的编程接口 (Assertion API)

每一个接口在设计时都考虑到了可扩展性,使得用户可以方便进行定制,保证测试用例的独立可维护。例如下面的代码

[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")];

用这段代码就可以开始对一个 Accessibility Identity 为 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_sufficientVisible()];

当然你可以把元素操作和断言都放在一个顺序中。例如,选择一个 Accessibility Identity 为 ClickMe 的元素,点击它,然后判断它是否显示可以这么写

[[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")] 
    performAction:grey_tap()] assertWithMatcher:grey_notVisible()];

以下是一个简单的交互模板

[[[EarlGrey selectElementWithMatcher:<element_matcher>]
    performAction:<action>]
    assertWithMatcher:<assertion_matcher>];

和都是 GREYMatchers.h 中的匹配器,为 GREYAction.h 中的操作方法。因为每一个匹配器或者操作都有短名称的方法,我们可以通过调用 grey_sufficientlyVisible() 来替代 [GREYMatchers matcherForSufficientlyVisible]。用短标记方式调用是默认允许的。可以通过在项目的头文件中添加 #define GREY_DISABLE_SHORTHAND 1 来禁止掉。

选中相关的编程接口 (Selection API)

使用 Selection API 来定位屏幕中的 UI 元素。当 API 接收到 Matcher 后会在所有 UI 分层中定位对应的元素来发生交互。在 EarlGrey 中,Matchers 支持类似OCHamcrest的 Matchers。同时也可以使用与或逻辑组合 Matchers,可以精准地定位到 UI 分层中的任意元素。

EarlGrey 匹配器 (EarlGrey Matchers)

所有的 EarlGrey 匹配器都可以在工厂类GREYMatchers中找到。定位元素最好的方法是通过的它的 accessibility properties。官方强烈推荐使用 accessibility identifier 因为它可以唯一标识一个元素。你也可以使用其他的 accessibility properties,例如使用 grey_accessibilityTrait() 来匹配具有特殊的 accessibility traits。再比如使用 grey_accessibilityLabel() 来匹配具有特定 accessibility labels。

一个匹配器可以匹配多个元素。例如 grey_sufficientlyVisible() 会匹配出所有的可见的 UI 元素。这种情况下,你必须缩小选取的范围知道唯一确定单个元素。你可以通过组合多个匹配器例如使用 grey_allOf(),grey_anyOf(),grey_not() 来使得你的匹配更加精准。还可以使用根匹配器 inRoot 来缩小选取范围。

我们来看下面的例子

第一个,通过选择器的组合,下面这段代码匹配的元素,以 Send 作为 accessibility label 并且显示在当前屏幕

id<GREYMatcher> visibleSendButtonMatcher = 
    grey_allOf(grey_accessibilityLabel(@"Send"),grey_sufficientlyVisible(),nil);
[[EarlGrey selectElementWithMatcher:visibleSendButtonMatcher]
    performAction:grey_tap()];

下一个,使用 inRoot,下面这段代码匹配的元素,以 Send 作为 accessibility label 并且被包含在一个 SendMessageView 实例化对象的 UI 元素下。

[[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"Send")]
    inRoot:grey_kindOfClass([SendMessageView class])] 
    performAction:grey_tap()];

自定义匹配器 (Custom Matchers)

通过使用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 类型。当匹配器用于选取元素时需要使用 id。以下代码普遍适用于 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];
}

这个匹配器可以用来测试选取没人任何子元素或者视图的 UI 元素并双击它 (假设这个方法命名为 CustomMatchers)

[[EarlGrey selectElementWithMatcher:[CustomMatchers matcherForElementWithoutChildren]]
    performAction:grey_doubleTap()];

选取隐藏的 UI 元素

在某些情况下 UI 元素是未显示在屏幕上的,需要一些操作后才会在屏幕中显示。常见的例子有,滚动到一个元素,但此元素还没显示在混动视图中;放大地图知道 UI 元素显示街景模式;UICollectionView 中自定义的布局等。你可以使用方法 [usingSearchAction:onElementWithMatcher:] 来提供基于这些元素的搜索功能。API 可以让你规定搜索操作并且在搜索操作下的元素进行匹配。EarlGrey 可以重复执行搜索操作直到定位到需要的元素 (或者超时)。

例如,下面的代码尝试通过匹配器 aButtonMatcher 定位元素,此元素在匹配器 aScrollViewMatcher 定位出的元素中,前一个匹配器会重复执行 (每次向下移动 50points) 直到找到按钮元素并点击

[[[EarlGrey selectElementWithMatcher:aButtonMatcher]
    usingSearchAction:grey_scrollInDirection(kGERYanDirectionDown,50)
    onElementWithMatcher:aSrollViewMatcher]
    performAction:grey_tap()];

下面的代码用来定位一个表格中的指定行,这一行满足 aCellMatcher,表格满足 aTableViewMatcher,未找到指定行前,会每次向下滚动 50points

[[[EarlGrey selectElementWithMatcher:aCellMatcher]
    usingSearchAction:grey_scrollInDirection(kGERYanDirectionDown,50)
    onElementWithMatcher:aTableViewMatcher]
    performAction:grey_tap()];

操作相关 API(Action API)

使用 Action API 来描述在选中元素上的测试操作

EarlGrey Actions

所有的 EarlGrey Actions 都在工厂类GREYAciton中。最常用的操作就是点击,通过方法 grey_tap() 调用

[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"TapMe")]
    performAction:grey_tap()];

如果操作修改了 UI 布局,EarlGrey 会同步对 UI 元素一系列操作中的每一次操作。在执行下一步操作时需要确定 UI 元素处于稳定的状态。

不是所有的操作都能在所有的元素上生效。例如,点击操作不能在一个不可见的元素上执行。为了避免这种情况,EarlGrey 会有限制,在每次真正执行操作时需要确保 GREYMatcher 必须定位到元素。没有遵循这个限制会导致抛出异常并且测试用例被标记为失败。为了避免测试用例执行失败,可以通过 performAction:error:这种调用方式获取 NSError 对象来获取错误详情。

NSError *error;
[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"TapMe")]
    performAciton:grey_tap()
    error:&error];

以下的例子中,应用中没有 Title 为"Tap me!"的 button,两种调用方式的区别。
2016_02_29_16_38_34

自定义操作 (Custom Actions)

遵循GREYAction协议可以自定义操作。为了方便,你可以使用GREYActionBlock,此文件已经遵循 GREYAction 协议让你能够快速的自定义操作。下面的代码就是用来自定义一个操作,用来 animate 选中的元素的窗口

- (id<GREYAction>)animateWindowAction {
  return [GREYActionBlock actionWithName:@"Animate Window"
                              constraint: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(Assertion API)

断言 API 用来核实 UI 元素的状态和行为。

使用匹配器断言 (Assertions Using Matchers)

使用 assertWithMatcher:通过GREYMatcher匹配器来进行断言。选中的元素能否通过匹配器来断言元素的状态。例如,下面的代码就是判断一个 accessibilityID 为 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:。在进行简单的断言时,匹配器更加轻量和理想。然而,自定义 GREYAssertion 在复杂的断言逻辑下会是更好的选择。例如操作 UI 再进行断言。

自定义断言 (Custom Assertions)

你可以通过GREYAssertion protocol 或者GREYAssertionBlock创建自定义断言。下面的代码就是使用 GREYAssertionBlock 来创建自定义断言来检查 view 的 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 元素的有效性在执行断言之前。例如,下面的代码就是在执行断言前判断元素是否存在

+ (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;
  }];
}

错误处理器 (Failure Handlers)

默认情况下,在发生异常时,EarlGrey 会使用框架自身的错误处理器。默认的处理器会记录异常,进行屏幕快照,打印其他任何有用信息的路径。你可以选择自定义自己的错误处理器通过 EarlGrey setFailureHandler:来安装在 API 上来取代全局默认的框架错误处理器。通过GREYFailureHandler protocol 来自定义错误处理器

@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

断言宏 (Assertions Macros)

EarlGrey 提供了自带的宏,在测试用例中用来断言和核实。这些宏和 XCTest 提供的宏类似,当断言失败时会调用全局的错误处理器。

  • GREYAssert(expression, reason, ...) 当表达式为 false 时失败

  • GREYAssertTrue(expression, reason, ...) 当表达式为 false 时失败

  • GREYAssertFalse(expression, reason, ...) 当表达式为 true 时失败

  • GREYAssertNotNil(expression, reason, ...) 当表达式为 nil 时失败

  • GREYAssertNil(expression, reason, ...) 当表达式为非 nil 值时失败

  • GREYAssertEqual(left, right, reason, ...) 当 left != right 时失败

  • GREYFail(reason, ...) 直接失败提供原因

  • GREYFailWithDetails(reason, details, ...) 直接失败提供原因和细节

布局测试 (Layout Testing)

EarlGrey 提供了 APIs 来检验 UI 元素布局。例如,判断元素 x 是否在元素 y 的左边。布局断言是根据NSLayoutConstraint的方式构建的。为了验证布局,你需要首先创建一个 constraint 来描述布局,然后选中的一个在描述中的元素,最后使用 grey_layout() 来断言选中的元素是否符合 constraint。需要注意的是 grey_note() 包括很多 constraints,都必须要满足。这样可以简单的描述出复杂的布局断言。

例如,下面的代码描述的就是选中的元素在所有其他元素的右边

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:创建一个简单的定向的 constraints。下面的代码和上面的效果一样。

GREYLayoutConstraint *rightConstraint =
    [GREYLayoutConstraint layoutConstraintForDirection:kGREYLayoutDirectionRight
                                  andMinimumSeparation:0.0];

同步相关 API(Synchronization APIs)

此类 API 用来控制 EarlGrey 和被测应用的同步。

GREYCondition

如果你的测试用例需要一些指定的处理,你可以使用 GREYCondition 来等待或者同步先决条件。GREYCondition 通过代码块来返回布尔值来显示先决条件是否已经符合。在执行其余测试用例之前框架会等待满足这个测试用例的先决条件发生。下面的代码说明了如何创建和使用 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 通过追踪主分发队列,主运行队列,网络,绘制和一些其他的信号来自动等待应用进入空闲状态并且只有在应用处于空闲状态时才会执行交互。可是有些场景下尽管应用处于忙碌状态也需要进行一些交互。例如在一个消息应用中,图片正在上传同时页面绘制也在进行但是测试用例可能同时需要输入并发送文字信息。需要处理这种场景时,你可以使用 kGREYConfigKeySynchronizationEnabled 关闭 EarlGrey 的同步功能,代码如下

[[GREYConfiguration sharedInstance] setValue:@(NO)
                                forConfigKey:kGREYConfigKeySynchronizationEnabled];

关闭同步功能之后所有的交互都不会等待应用进入空闲状态下再执行直到此功能重新打开。注意为了提高测试的有效性,同步功能要紧张重新打开。另外也可以不关闭此功能,通过配置同步功能的参数来达到测试用例所需要的场景。例如

  • kGREYConfigKeyNSTimerMaxTrackableInterval 用来设置非重复定时器之间的最大间隔

  • kGREYConfigKeyDispatchAfterMaxTrackableDelay 用来设置下一步操作的最大延迟时间

  • kGREYConfigKeyURLBlacklistRegex 用来设置不需同步的 URL 黑名单正则式

网络

默认情况下,EarlGrey 会同步所有的网络请求,但是你可以通过 URL 正则式来自定义无需同步的请求。注意尽管只有一个正则式,也可以用来将多个 URL 排除在外。如果需要过滤不同域名下的网络请求,可以通过逻辑操作符 or(|)。例如 (http://www.google.com|http://www.youtube.comhttp://www.google.com 和 http://www.youtube.com。设置方式如下) 会排除

[[GREYConfiguration sharedInstance] setValue:@".*\\.foo\\.com/.*"
                                forConfigKey:kGREYConfigKeyURLBlacklistRegex];

交互超时

默认情况下,交互的超时时间为 30s。在这种情况下,如果被测应用没有进入空闲状态,会抛出超时异常并且测试用例并置为失败。你可以使用GREYConfiguration来修改超时时间的值。例如设置超时时间为 60s

[[GREYConfiguration sharedInstance] setValue:@(60.0)
                                forConfigKey:kGREYConfigKeyInteractionTimeoutDuration];

绘制超时

界面绘制会影响同步,因为它总是无限制的重复执行或者执行很长时间。绘制时间大于测试超时时间会抛出超时异常。为了避免这种情况,EarlGrey 限制绘制时间为 5s 并且关闭了重复绘制 (连续绘制只执行一次)。可以让 EarlGrey 绘制更长的时间,修改允许绘制的最大时间值。确保它不会影响到测试的超时时间。例如设置最大绘制时间为 30s

[[GREYConfiguration sharedInstance] setValue:@(30.0)
                                forConfigKey:kGREYConfigKeyCALayerMaxAnimationDuration];

非主线程调用

由于分发队列的限制和 EarlGrey 与此队列同步方式,从分发队列调用 EarlGrey 代码会导致活锁。为了减少这种情况,可以使用基于块的 APIs 来包装 EarlGrey 语句让其被安全调用

  • grey_execute_sync(void (block)()) 同步

  • grey_execute_async(void (block)()) 异步

其他的编程接口 (Other Top Level APIs)

除了 UI 的操作,你可以使用很多种方式通过 EarlGrey 控制设备和系统。

全局配置 (Global Configuration)

GREYConfiguration 可以让你配置 EarlGrey 框架。这提供了配置同步,交互超时,操作限制,日志等的功能。只要配置修改,就会全局生效。例如下面就是打开详细日志的方法

[[GREYConfiguration sharedInstance] setValue:@(YES)
                                forConfigKey:kGREYConfigKeyVerboseLogging];

控制设备朝向

使用 [EarlGrey rotateDeviceToOrientation:errorOrNil:] 来翻转设配让设配处于一个特定的位置。下面的代码可以使设配从竖屏模式翻转到横屏模式并且 HOME 键在右侧

[EarlGrey rotateDeviceToOrientation:UIDeviceOrientationLandscapeLeft errorOrNil:nil];

可选择的模式如下

  • UIDeviceOrientationUnknown

  • UIDeviceOrientationPortrait

  • UIDeviceOrientationPortraitUpsideDown

  • UIDeviceOrientationLandscapeLeft

  • UIDeviceOrientationLandscapeRight

  • UIDeviceOrientationFaceUp

  • UIDeviceOrientationFaceDown

参考资料

EarlGrey API

共收到 6 条回复 时间 点赞

赞~写得很全。

有两个小建议:

  1. markdown 的无序列表上下需要留一个空行,否则会像现在正文那样出现星号
  2. 最后的 控制设配朝向 这里,“设配” 应该是 “设备” 吧?

#1 楼 @chenhengjie123 多谢恒捷老师指正,已做修改

#2 楼 @hillchan31 我不是老师。。。大家互相学习就好~

#3 楼 @chenhengjie123 哈哈 在帖子[Google EarlGrey] 0x00 安装及运行已经拜过师了,学习到了知识就是老师

匿名 #5 · 2016年04月27日

刚好要做这一块的方案实践,看到这个分享必须赞

hillchan31 [Google EarlGrey] 0x00 安装及运行 中提及了此贴 04月13日 14:32
hillchan31 [Google EarlGrey] 0x01 第一个测试用例 中提及了此贴 04月13日 21:00

你好,我想问一下,程序中涉及系统弹出的确定按钮,要如何定位选择

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册