iOS 自動化測試框架 Google EarlGrey 嘗鮮
今天看到恒溫發的鏈接,加上最近項目在做 iOS 自動化框架的調研,趕緊嘗鮮了下。
框架主頁:https://github.com/google/EarlGrey
<h2 id="框架簡介">框架簡介</h2>
<p>EarlGrey is a native iOS UI automation test framework that enables you to write clear, concise tests.</p>
<p>With the EarlGrey framework, you have access to enhanced synchronization features. EarlGrey automatically synchronizes with the UI, network requests, and various queues, but still allows you to manually implement customized timings, if needed.</p>
<p>EarlGrey’s synchronization features help ensure that the UI is in a steady state before actions are performed. This greatly increases test stability and makes tests highly repeatable.</p>
<p>EarlGrey works in conjunction with the XCTest framework and integrates with Xcode’s Test Navigator so you can run tests directly from Xcode or the command line (using xcodebuild).</p>
<p>特性:</p>
<ul>
<li>Synchronization</li>
</ul>
<p>Typically, you shouldn’t be concerned about synchronization as EarlGrey automatically synchronizes with the UI, network requests, main Dispatch Queue, and the main NSOperationQueue. To support cases where you want to wait for some event to occur before the next UI interaction happens, EarlGrey provides Synchronization APIs that allow you to control EarlGrey's synchronization behavior. You can use these APIs to increase the stability of your tests.</p>
<ul>
<li>Visibility Checks</li>
</ul>
<p>EarlGrey uses screenshot differential comparison (also known as 'screenshot diffs') to determine the visibility of UI elements before interacting with them. As a result, you can be certain that a user can see and interact with the UI that EarlGrey interacts with.</p>
<p>Note: Out-of-process (i.e. system generated) alert views and other modal dialogs that obscure the UI can interfere with this process.</p>
<ul>
<li>User-Like Interaction</li>
</ul>
<p>Taps and swipes are performed using app-level touch events, instead of using element-level event handlers. Before every UI interaction, EarlGrey asserts that the elements being interacted with are actually visible (see Visibility Checks) and not just present in the view hierarchy. EarlGrey's UI interactions simulate how a real user would interact with your app's UI, and help you to find and fix the same bugs that users would encounter in your app.</p>
<p>簡而言之,是一個內嵌式框架(以 framework 形式內嵌至應用中),用例繼承 XCTestCase ,本質上是 iOS 的 Unit test 。比較類似的框架是 KIF 。</p>
<p>主要特性是:</p>
<ul>
<li>同步性:需要等待的操作自動等待,媽媽不用擔心我的 wait 和 sleep 寫錯了</li>
<li>可見性檢測:因為是直接對應用內對象操作,所以有可能給一個用戶看不到的元素發送觸控事件了。這個可以防止出現這種情況,瀏覽器使用的 Webdriver 里面也有類似特性</li>
<li>模擬用戶操作:使用 app 級別的觸控對象,而非元素級別的事件觸發。簡而言之,屏幕上不可見的元素都操作不了了。</li>
</ul>
<h2 id="嘗鮮準備">嘗鮮準備</h2>
<p>因為它的 Prerequisites 略復雜,所以直接用了官方 Example 。</p>
<p>環境需求:<br />
- Xcode
CocoaPod (1.0.0 beta 或者 0.39 stable 均可)</p>
1、
git clone https://github.com/google/EarlGrey.git
2、 在EarlGrey/Demo/EarlGreyExample
執行pod install
安裝依賴庫。如果你的 Pod 是 1.0.0 beta ,恭喜你,直接運行即可。如果是 0.39 stable ,請改成下面的內容:# # Copyright 2016 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License.
EarlGreyExample
platform :ios, '8.0'
<h2 id="示例用例執行及解析">示例用例執行及解析</h2>
<p>應用支持模擬器,直接在模擬器上 Test 了一遍,全部通過。</p>
<p>官方給了兩套用例,一套是 OC 寫的,另一套是 swift 寫的。內容一樣。里面的寫法很有學習價值。這里以 OC 為例,簡單記錄一下。</p>
<pre class="brush:cpp; toolbar: true; auto-links: false;">- (void)testBasicSelection {
// Select the button with Accessibility ID "clickMe". [EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]; }
(void)testBasicSelectionAndAction { // Select and tap the button with Accessibility ID "clickMe". [[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
performAction:grey_tap()];
}
(void)testBasicSelectionAndAssert { // Select the button with Accessibility ID "clickMe" and assert it's visible. [[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
assertWithMatcher:grey_sufficientlyVisible()];
}
(void)testBasicSelectionActionAssert { // Select and tap the button with Accessibility ID "clickMe", then assert it's visible. [[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
performAction:grey_tap()] assertWithMatcher:grey_sufficientlyVisible()];
}</pre>
循序漸進,從找元素、對元素操作、找元素+斷言、對元素操作+斷言四個階段編寫。從這些用例看出,EarlGrey 的 API 中找元素與元素操作是分離的,而非像 KIF 那樣合并在一起。
- (void)testSelectionOnMultipleElements { // This test will fail because both buttons are visible and match the selection. // We add a custom error here to prevent the Test Suite failing. NSError *error; [[EarlGrey selectElementWithMatcher:grey_sufficientlyVisible()]
performAction:grey_tap() error:&error];
if (error) { NSLog(@"Test Failed with Error : %@",[error description]); } }</pre>
展示了如何捕獲 ERROR (寫法和一些老的 UIKit 函數類似,返回的是 error 的地址而非內容)。這里的 error 原因是有不止一個匹配的元素。
- (void)testCollectionMatchers { id<GREYMatcher> visibleSendButtonMatcher =
grey_allOf(grey_accessibilityID(@"ClickMe"), grey_sufficientlyVisible(), nil);
[[EarlGrey selectElementWithMatcher:visibleSendButtonMatcher]
performAction:grey_doubleTap()];
}</pre>
展示了如何使用多條件獲取元素。例子中的
grey_allOf(grey_accessibilityID(@"ClickMe"), grey_sufficientlyVisible(), nil)
是指這個 matcher 的獲取條件為:AccessibilityID = "ClickMe" AND visible
。最后的 nil 應該只是展示支持 nil 。- (void)testWithInRoot { // Second way to disambiguate: use inRoot to focus on a specific window or container. // There are two buttons with accessibility id "Send", but only one is inside SendMessageView. [[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"Send")]
inRoot:grey_kindOfClass([SendMessageView class])] performAction:grey_doubleTap()];
}</pre>
展示 inRoot 父 view 篩選器。視圖中有兩個元素有相同的 AccessibilityId,但其中一個父 view 是 SendMessageView 類型的。
// Define a custom matcher for table cells that contains a date for a Thursday. - (id<GREYMatcher>)matcherForThursdays { MatchesBlock matches = ^BOOL(UIView *cell) { if ([cell isKindOfClass:[UITableViewCell class]]) {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.dateStyle = NSDateFormatterLongStyle; NSDate *date = [formatter dateFromString:[[(UITableViewCell *)cell textLabel] text]]; NSCalendar *calendar = [NSCalendar currentCalendar]; NSInteger weekday = [calendar component:NSCalendarUnitWeekday fromDate:date]; return weekday == 5;
} else {
return false;
} }; DescribeToBlock describe = ^void(id<GREYDescription> description) { [description appendText:@"Date for a Thursday"]; };
return [[GREYElementMatcherBlock alloc] initWithMatchesBlock:matches
descriptionBlock:describe];
}
(void)testWithCustomMatcher { // Use the custom matcher. [[EarlGrey selectElementWithMatcher:[self matcherForThursdays]]
performAction:grey_doubleTap()];
}</pre>
自定義 matcher 。有兩個部分。matcherBlock部分如果 block return true 就匹配,false 就不匹配。descriptionBlock 則是這個 matcher 的描述。用于 GREYBaseMatcher::describeTo:
- (void)testTableCellOutOfScreen { // Go find one cell out of the screen. [[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"Cell30")]
usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, 50)
onElementWithMatcher:grey_accessibilityID(@"table")]
performAction:grey_doubleTap()];
// Move back to top of the table. [[[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"Cell1")]
usingSearchAction:grey_scrollInDirection(kGREYDirectionUp, 500)
onElementWithMatcher:grey_accessibilityID(@"table")]
performAction:grey_doubleTap()];
}</pre>
獲取屏幕外部元素。usingSearchAction:onElementWithMatcher 可以在父元素內通過指定 action (例子中用的是滑動)遍歷元素來查找指定元素。
- (void)testCatchErrorOnFailure { // TapMe doesn't exist, but the test doesn't fail because we are getting a pointer to the error. NSError *error; [[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"TapMe")]
performAction:grey_tap() error:&error];
if (error) { NSLog(@"Error: %@", [error localizedDescription]); } }</pre>
同樣是捕獲 Error ,只是為了說明當元素找不到時也會產生 error 。
// Fade in and out an element. - (void)fadeInAndOut:(UIView *)element { [UIView animateWithDuration:1.0
delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations: ^{ element.alpha = 0.0;} completion: ^(BOOL finished) { [UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveEaseIn animations: ^{ element.alpha = 1.0;} completion: nil]; }];
}
// Define a custom action that applies fadeInAndOut to the selected element. - (id<GREYAction>)tapClickMe { return [GREYActionBlock actionWithName:@"Fade In And Out" constraints:nil performBlock: ^(id element, NSError __strong errorOrNil) { // First make sure element is attached to a window. if ([element window] == nil) { NSDictionary errorInfo = @{ NSLocalizedDescriptionKey: NSLocalizedString(@"Element is not attached to a window", @"")}; errorOrNil = [NSError errorWithDomain:kGREYInteractionErrorDomain code:1 userInfo:errorInfo]; return NO; } else { [self fadeInAndOut:[element window]]; return YES; } }]; }
(void)testCustomAction { // Test using the custom action tapClickMe. [[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
performAction:[self tapClickMe]];
}</pre>
代碼略多,主要是為了展示封裝能力。GREYActionBlock 能把元素傳到 performBlock 的 element 參數,用于對元素執行指定操作。
// Write a custom assertion that checks if the alpha of an element is equal to the expected value. - (id<GREYAssertion>)alphaEqual:(CGFloat)expectedAlpha { return [GREYAssertionBlock assertionWithName:@"Assert Alpha Equal"
assertionBlockWithError:^BOOL(UIView *element, NSError *__strong *errorOrNil) { // Assertions can be performed on nil elements. Make sure view isn’t nil. if (element == nil) { *errorOrNil = [NSError errorWithDomain:kGREYInteractionErrorDomain code:kGREYInteractionElementNotFoundErrorCode userInfo:nil]; return NO; } return element.alpha == expectedAlpha; }];
}
(void)testWithCustomAssertion { [[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
assert:([self alphaEqual:1.0])];</pre>
自定義 assert 。類型必須是 id<GREYAssertion> ,返回值為 GREYAssertionBlock 類型,包含 assert 的描述及實際 assert 的 block 。
- (void)handleException:(GREYFrameworkException )exception details:(NSString )details { NSLog(@"Test Failed With Reason : %@ and details : %@", [exception reason], details); } ...
(void)testWithCustomFailureHandler { // This test will fail and use our custom handler to handle the failure. // The custom handler is defined at the beginning of this file. PrintOnlyHandler *myHandler = [[PrintOnlyHandler alloc] init]; [EarlGrey setFailureHandler:myHandler]; [[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"TapMe")]
performAction:(grey_tap())];
}</pre>
自定義 FailtureHandler 。這個是全局的 handler ,很適合用來做 fail 后截圖+保存日志等操作。要注意 exception 類型為 GREYFrameworkException 。
- (void)testLayout { // Define a layout constraint. GREYLayoutConstraint *onTheRight =
[GREYLayoutConstraint layoutConstraintWithAttribute:kGREYLayoutAttributeLeft relatedBy:kGREYLayoutRelationGreaterThanOrEqual toReferenceAttribute:kGREYLayoutAttributeRight multiplier:1.0 constant:0.0];
[[EarlGrey selectElementWithMatcher:grey_accessibilityLabel(@"SendForLayoutTest")]
assertWithMatcher:grey_layout(@[onTheRight], grey_accessibilityID(@"ClickMe"))];
}</pre>
可以封裝自動布局的 constraint 斷言。這是一個不錯的功能,實際上更偏向于 Unit Test 。
- (void)testWithCondition { GREYCondition *myCondition = [GREYCondition conditionWithName: @"Example condition" block: ^BOOL { int i = 1; while (i <= 100000) {
i++;
} return YES; }]; // Wait for my condition to be satisfied or timeout after 5 seconds. BOOL success = [myCondition waitWithTimeout:5]; if (!success) { // Just printing for the example. NSLog(@"Condition not met"); } else { [[EarlGrey selectElementWithMatcher:grey_accessibilityID(@"ClickMe")]
performAction:grey_tap()];
} }</pre>
最后一個用例。估計作者寫不下去了,condition 竟然直接就是 i++ 。。。這個純粹用來演示 GREYCondition + waitWithTimeout 的封裝,實際用途應該是用于網絡請求超時檢測什么的。
感受
一個不小心把示例用例全部看了個遍,感受還是比較深的。簡單地說,這個 API 的 Exception 、 condition、 matcher 都封裝了 description ,對于測試報告和記錄比較友好。同時它本身提到的三個特點對于 UI 測試也比較實用。只是只能向后兼容到 iOS 8.0 這個有點悲催。
不過目前還沒有嘗試把它加入到一個實際項目中,待晚些嘗試一下,看看它的 Prerequisites 是否不那么容易滿足,pod 集成是否有坑。
</article>
</div>