iOS自動化測試之KIF使用分享

weixin_34320159發表於2017-07-31

KIF的全稱是Keep it functional。它是一個建立在XCTest的UI測試框架,通過accessibility來定位具體的控制元件,再利用私有的API來操作UI。由於是建立在XCTest上的,所以你可以完美的藉助XCode的測試相關工具(包括命令列指令碼)。

特點:

  • 最小化迂迴時間
    繼承KIFTestCase,測試程式碼都是使用OC編寫,最大程度減少了中間層。
  • 配置簡單
    直接整合到XCode上,不需要安裝多餘的包。
  • 像使用者一樣測試
    測試程式碼模仿使用者操作,程式碼很簡單
  • 自動整合XCode 5以上的測試工具
    在XCode上使用就像使用蘋果原生的測試框架一樣,支援XCode的各種測試工具。

按照KIF的的官方文件進行匯入KIF,接下來以官方的demo為例,進行KIF的相關操作的講解。

1940317-fc101c9ccbc4140d.jpg

目錄

  • 使用描述
  • Tapping
  • Show/Hide
  • Gesture
  • TableView
  • Picker
  • ModalView
  • CollectionView
  • ScrollView
  • Landscape
  • WebView
  • Background
  • PullToRefesh
  • CascadingFailure
  • 補充Tips

使用描述

使用KIF主要有兩個核心類:

  • KIFTestCase XCTestCase的子類
  • KIFUITestActor 控制UI,常見的三種是:點選一個View,向一個View輸入內容,等待一個View的出現

KIF利用Accessibility來找元素.tapViewWithAccessibilityLabel 這也許是最常被用到的測試動作方法。正如其名稱所顯示的,它可以在給定的輔助標籤模擬在檢視上的觸擊。在大多數情況下,輔助標籤和可視的文字標籤(例如按鈕元件)是配套的。否則你就需要手動設定輔助標籤.
一些控制元件,諸如 UISwitch,更加複雜,需要比簡單的觸擊更復雜的步驟來觸發。KIF 提供了一個特殊的 setOn:forSwitchWithAccessibilityLabel: 方法來改變一個切換的狀態.

匯入KIF框架後,在專案的Tests(如果還沒有Tests資料夾,可以先建立Tests)宣告一個測試類,必須以Tests結尾(如LoginTests),繼承自KIFTestCase。為了讓程式在執行每個測試用例時能保證連貫性,在每個類中都宣告兩個方法beforeEachafterEach 保證測試過程中可以進入想要執行的測試頁面與退出測試頁面。

///進入指定頁面
- (void)beforeEach{
    [tester tapViewWithAccessibilityLabel:@"Tapping"];
}
///退出指定頁面
- (void)afterEach{
    [tester tapViewWithAccessibilityLabel:@"Test Suite" traits:UIAccessibilityTraitButton];
}

Tapping

1940317-837f0069d7b94f67.jpg

點選控制器上標記為TapViewController Inner ScrollView的檢視上的元素

- (void)testTappingViewFromSpecificView{
    UIView *scrollView = [tester waitForViewWithAccessibilityIdentifier:@"TapViewController Inner ScrollView"];
    UIView *buttonView;
    UIAccessibilityElement *element;
    [tester waitForAccessibilityElement:&element view:&buttonView withIdentifier:@"Inner Button" fromRootView:scrollView tappable:YES];
    
    if (buttonView != NULL) {
        [tester tapAccessibilityElement:element inView:buttonView];
    }
}

長按操作

- (void)testLongPressingViewViewWithTraits{
    [tester longPressViewWithAccessibilityLabel:@"Greeting" value:@"Hello" duration:2];
    [tester tapViewWithAccessibilityLabel:@"Select All"];
}

切換按鈕狀態

- (void)testTogglingASwitch
{
    [tester waitForViewWithAccessibilityLabel:@"Happy" value:@"1" traits:UIAccessibilityTraitNone];
    [tester setOn:NO forSwitchWithAccessibilityLabel:@"Happy"];
    [tester waitForViewWithAccessibilityLabel:@"Happy" value:@"0" traits:UIAccessibilityTraitNone];
    [tester setOn:YES forSwitchWithAccessibilityLabel:@"Happy"];
    [tester waitForViewWithAccessibilityLabel:@"Happy" value:@"1" traits:UIAccessibilityTraitNone];
}

滑動狀態條

- (void)testMovingASlider{
    [tester waitForTimeInterval:1];
    [tester setValue:3 forSliderWithAccessibilityLabel:@"Slider"];
    [tester waitForViewWithAccessibilityLabel:@"Slider" value:@"3" traits:UIAccessibilityTraitNone];
    [tester setValue:0 forSliderWithAccessibilityLabel:@"Slider"];
    [tester waitForViewWithAccessibilityLabel:@"Slider" value:@"0" traits:UIAccessibilityTraitNone];
    [tester setValue:5 forSliderWithAccessibilityLabel:@"Slider"];
    [tester waitForViewWithAccessibilityLabel:@"Slider" value:@"5" traits:UIAccessibilityTraitNone];
}

選取系統的圖片

- (void)testPickingAPhoto{
    [tester tapViewWithAccessibilityLabel:@"Photos"];
    [tester acknowledgeSystemAlert];
    [tester waitForTimeInterval:0.5f]; // Wait for view to stabilize
    [tester choosePhotoInAlbum:@"Camera Roll" atRow:1 column:2];
    [tester waitForViewWithAccessibilityLabel:@"UIImage"];
}

Show/Hide

一直尋找區域內標記為B的值為BB的可點選的按鈕

1940317-ab6429e539e26ba1.jpg
- (void)testWaitingForViewWithValue{
    NSLog(@"testWaitingForViewWithValue");
    [tester waitForTappableViewWithAccessibilityLabel:@"B" value:@"BB" traits:UIAccessibilityTraitButton];
}

選擇檢視內可點選切換狀態的按鈕

- (void)testTappingOnlyIfNotSelected{
    [tester tapViewIfNotSelected:@"A"];
    [tester waitForViewWithAccessibilityLabel:@"A" traits:UIAccessibilityTraitSelected];
    
    // This should not deselect the element.
    [tester tapViewIfNotSelected:@"A"];
    [tester waitForViewWithAccessibilityLabel:@"A" traits:UIAccessibilityTraitSelected];
}
- (void)tapViewIfNotSelected:(NSString *)label{
    UIAccessibilityElement *element;
    UIView *view;
    [self waitForAccessibilityElement:&element view:&view withLabel:label value:nil traits:UIAccessibilityTraitNone tappable:YES];
    if ((element.accessibilityTraits & UIAccessibilityTraitSelected) == UIAccessibilityTraitNone) {
        [self tapAccessibilityElement:element inView:view];
    }
}

Gesture

1940317-dc1b638250df25fe.jpg

手勢操作

滑動

四個方向都可操作

typedef NS_ENUM(NSUInteger, KIFSwipeDirection) {
    KIFSwipeDirectionRight,
    KIFSwipeDirectionLeft,
    KIFSwipeDirectionUp,
    KIFSwipeDirectionDown
};

示例

- (void)testSwipingRight{
    [tester swipeViewWithAccessibilityLabel:@"Swipe Me" inDirection:KIFSwipeDirectionRight];
    [tester waitForViewWithAccessibilityLabel:@"Right"];
}

點選

- (void)testPanningRight{
    NSString* regexPattern = kPanRightRegex;
    NSPredicate *resultTestPredicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regexPattern];
    NSPredicate *noVelocityPredicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", KPanNoVelocityValue];
    
    UIView* velocityResultView = [tester waitForViewWithAccessibilityLabel:kVelocityValueLabelAccessibilityString];
    XCTAssertTrue([velocityResultView isKindOfClass:[UILabel class]], @"Found view is not a UILabel instance!");
    UILabel* velocityLabel = (UILabel*)velocityResultView;
    
    UIView* panLabel = [tester waitForTappableViewWithAccessibilityLabel:kPanMeAccessibilityString];
    CGPoint centerInView = CGPointMake(panLabel.frame.size.width / 2.0, panLabel.frame.size.height / 2.0);
    
    [panLabel dragFromPoint:centerInView toPoint:CGPointMake(centerInView.x + 30, centerInView.y)];
    XCTAssertFalse([noVelocityPredicate evaluateWithObject:velocityLabel.text], @"No valocity value found!");
    XCTAssertTrue([resultTestPredicate evaluateWithObject:velocityLabel.text], @"The result doesn`t match the %@ regex pattern", regexPattern);
}

拖拽

- (void)testScrolling{
    // Needs to be offset from the edge to prevent the navigation controller's interactivePopGestureRecognizer from triggering
    [tester scrollViewWithAccessibilityIdentifier:@"Scroll View" byFractionOfSizeHorizontal:-0.80 vertical:-0.80];
    [tester waitForTappableViewWithAccessibilityLabel:@"Bottom Right"];
    [tester scrollViewWithAccessibilityIdentifier:@"Scroll View" byFractionOfSizeHorizontal:0.80 vertical:0.80];
    [tester waitForTappableViewWithAccessibilityLabel:@"Top Left"];
}

TableView

1940317-8e18790c2f625150.jpg

tableView的手勢滑動

 [tester swipeRowAtIndexPath:firstCellPath inTableView:tableView inDirection:KIFSwipeDirectionLeft];

點選tableview上指定的某個cell

- (void)testTappingRows{
    [tester tapRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:2] inTableViewWithAccessibilityIdentifier:@"TableView Tests Table"];
    [tester waitForViewWithAccessibilityLabel:@"Last Cell" traits:UIAccessibilityTraitSelected];
    [tester tapRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] inTableViewWithAccessibilityIdentifier:@"TableView Tests Table"];
    [tester waitForViewWithAccessibilityLabel:@"First Cell" traits:UIAccessibilityTraitSelected];
}

移動

- (void)testMoveRowUpUsingNegativeRowIndexes{
    [tester moveRowAtIndexPath:[NSIndexPath indexPathForRow:-1 inSection:1]
 toIndexPath:[NSIndexPath indexPathForRow:-3 inSection:1] inTableViewWithAccessibilityIdentifier:@"TableView Tests Table"];
}

刪除

///刪除第一個cell
- (void)testSwipingRows {
    
    UITableView *tableView;
    [tester waitForAccessibilityElement:NULL view:&tableView withIdentifier:@"TableView Tests Table" tappable:NO];
    [tester waitForAnimationsToFinish];
    // First row
    NSIndexPath *firstCellPath = [NSIndexPath indexPathForRow:0 inSection:0];
    [tester swipeRowAtIndexPath:firstCellPath inTableView:tableView inDirection:KIFSwipeDirectionLeft];
    [tester waitForDeleteStateForCellAtIndexPath:firstCellPath inTableView:tableView];
    [tester tapViewWithAccessibilityLabel:@"Delete"];
    
    __KIFAssertEqualObjects([tester waitForCellAtIndexPath:firstCellPath inTableViewWithAccessibilityIdentifier:@"TableView Tests Table"].textLabel.text, @"Deleted", @"");
}

設定Button狀態

- (void)testTogglingSwitch{
    [tester setOn:NO forSwitchWithAccessibilityLabel:@"Table View Switch"];
    [tester setOn:YES forSwitchWithAccessibilityLabel:@"Table View Switch"];
}

檢查元素是否在檢視上 當元素在檢視上消失時 執行操作

- (void)testButtonAbsentAfterRemoveFromSuperview{
    UIView *view = [tester waitForViewWithAccessibilityLabel:@"Button"];
    [view removeFromSuperview];
    [tester waitForAbsenceOfViewWithAccessibilityLabel:@"Button"];
}

Picker

1940317-2b2c459708728ac5.jpg

時間選擇器

- (void)testSelectingDateInPast{
    [tester tapViewWithAccessibilityLabel:@"Date Selection"];
    NSArray *date = @[@"June", @"17", @"1965"];
    // If the UIDatePicker LocaleIdentifier would be de_DE then the date to set
    // would look like this: NSArray *date = @[@"17.", @"Juni", @"1965"
    [tester selectDatePickerValue:date];
    [tester waitForViewWithAccessibilityLabel:@"Date Selection" value:@"Jun 17, 1965" traits:UIAccessibilityTraitNone];
}

選擇器

- (void)testSelectingAPickerRow{
    [tester selectPickerViewRowWithTitle:@"Charlie"];
    NSOperatingSystemVersion iOS8 = {8, 0, 0};
   [tester waitForViewWithAccessibilityLabel:@"Call Sign" value:@"Charlie" traits:UIAccessibilityTraitNone];
}

ModalView

1940317-bccc30d89a322785.jpg

點選alertView上的選項 取消

- (void)testInteractionWithAnAlertView{
    [tester tapViewWithAccessibilityLabel:@"UIAlertView"];
    [tester waitForViewWithAccessibilityLabel:@"Alert View"];
    [tester waitForViewWithAccessibilityLabel:@"Message"];
    [tester waitForTappableViewWithAccessibilityLabel:@"Cancel"];
    [tester waitForTappableViewWithAccessibilityLabel:@"Continue"];
    [tester tapViewWithAccessibilityLabel:@"Continue"];
    [tester waitForAbsenceOfViewWithAccessibilityLabel:@"Message"];
}

點選底部彈窗

- (void)testInteractionWithAnActionSheet{
    [tester tapViewWithAccessibilityLabel:@"UIActionSheet"];
    [tester waitForViewWithAccessibilityLabel:@"Action Sheet"];
    [tester waitForTappableViewWithAccessibilityLabel:@"Destroy"];
    [tester waitForTappableViewWithAccessibilityLabel:@"A"];
    [tester waitForTappableViewWithAccessibilityLabel:@"B"];
}

點選獲取系統的activity

- (void)testInteractionWithAnActivityViewController{
    if (!NSClassFromString(@"UIActivityViewController")) {
        return;
    }
    [tester tapViewWithAccessibilityLabel:@"UIActivityViewController"];
    [tester waitForTappableViewWithAccessibilityLabel:@"Copy"];
    [tester waitForTappableViewWithAccessibilityLabel:@"Mail"];
}

CollectionView

1940317-f5bddbb7231205f2.jpg

點選Item

- (void)testTappingItems{
    [tester tapItemAtIndexPath:[NSIndexPath indexPathForItem:199 inSection:0] inCollectionViewWithAccessibilityIdentifier:@"CollectionView Tests CollectionView"];
    [tester waitForViewWithAccessibilityLabel:@"Last Cell" traits:UIAccessibilityTraitSelected];
    [tester tapItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] inCollectionViewWithAccessibilityIdentifier:@"CollectionView Tests CollectionView"];
    [tester waitForViewWithAccessibilityLabel:@"First Cell" traits:UIAccessibilityTraitSelected];
}

ScrollView

1940317-32ebec2f519a98ee.jpg

滑動

///down up right left 為ScrollView上的控制元件
- (void)testScrollingToTapOffscreenViews{
    [tester tapViewWithAccessibilityLabel:@"Down"];
    [tester tapViewWithAccessibilityLabel:@"Up"];
    [tester tapViewWithAccessibilityLabel:@"Right"];
    [tester tapViewWithAccessibilityLabel:@"Left"];
}

Landscape

切換橫屏

- (void)beforeAll{
    [system simulateDeviceRotationToOrientation:UIDeviceOrientationLandscapeLeft];
    [tester scrollViewWithAccessibilityIdentifier:@"Test Suite TableView" byFractionOfSizeHorizontal:0 vertical:-0.2];
}
- (void)afterAll{
    [system simulateDeviceRotationToOrientation:UIDeviceOrientationPortrait];
    [tester waitForTimeInterval:0.5];
}

TextField

1940317-41f237d9e5dce312.jpg

textField輸入文字

登入註冊時常常使用這種方式進行行為描述

- (void)testWaitingForSearchFieldToBecomeFirstResponder{
    [tester tapViewWithAccessibilityLabel:nil traits:UIAccessibilityTraitSearchField];
    [tester waitForFirstResponderWithAccessibilityLabel:nil traits:UIAccessibilityTraitSearchField];
    [tester enterTextIntoCurrentFirstResponder:@"text"];
    [tester waitForViewWithAccessibilityLabel:nil value:@"text" traits:UIAccessibilityTraitSearchField];
}

WebView

1940317-497cea04208a0ce8.jpg

點選連結

- (void)testTappingLinks {
    [tester tapViewWithAccessibilityLabel:@"A link"];
    [tester waitForViewWithAccessibilityLabel:@"Page 2"];
}

輸入文字

- (void)testEnteringText {
    [tester tapViewWithAccessibilityLabel:@"Input Label"];
    [tester enterTextIntoCurrentFirstResponder:@"Keyboard text"];
}

滾動Scroll

- (void)testScrolling {
    // Off screen, the web view will need to be scrolled down
    [tester waitForViewWithAccessibilityLabel:@"Footer"];
}

Background

進入後臺操作

- (void)testBackgroundApp {
    [tester waitForViewWithAccessibilityLabel:@"Start"];
    [system deactivateAppForDuration:5];
    [tester waitForViewWithAccessibilityLabel:@"Back"];
}

PullToRefesh

1940317-f9eace8aec2aedf8.jpg

下拉重新整理

-(void) testPullToRefreshByAccessibilityLabelWithDuration{
    UITableView *tableView;
    [tester waitForAccessibilityElement:NULL view:&tableView withIdentifier:@"Test Suite TableView" tappable:NO];

    [tester pullToRefreshViewWithAccessibilityLabel:@"Table View" pullDownDuration:KIFPullToRefreshInAboutThreeSeconds];
    [tester waitForViewWithAccessibilityLabel:@"Bingo!"];
    [tester waitForAbsenceOfViewWithAccessibilityLabel:@"Bingo!"];

    [tester waitForTimeInterval:5.0f]; //make sure the PTR is finished.
}

Typing

1940317-190ba1f49e2ba364.jpg

複製貼上

- (void)testEnteringTextIntoViewWithAccessibilityLabel{
    [tester longPressViewWithAccessibilityLabel:@"Greeting" value:@"Hello" duration:2];
    [tester tapViewWithAccessibilityLabel:@"Select All"];
    [tester tapViewWithAccessibilityLabel:@"Cut"];
    [tester enterText:@"Yo" intoViewWithAccessibilityLabel:@"Greeting"];
    [tester waitForViewWithAccessibilityLabel:@"Greeting" value:@"Yo" traits:UIAccessibilityTraitNone];
}

清除

- (void)testClearingAndEnteringTextIntoViewWithAccessibilityLabel
{
    [tester clearTextFromAndThenEnterText:@"Yo" intoViewWithAccessibilityLabel:@"Greeting"];
}

兩個textField聯動

- (void)testEnteringReturnCharacterIntoViewWithAccessibilityLabel
{
    [tester enterText:@"Hello\n" intoViewWithAccessibilityLabel:@"Other Text"];
    [tester waitForFirstResponderWithAccessibilityLabel:@"Greeting"];
    [tester enterText:@", world\n" intoViewWithAccessibilityLabel:@"Greeting" traits:UIAccessibilityTraitNone expectedResult:@"Hello, world"];
}

輸入Emoj表情

- (void)testEnteringEmojiCharactersIntoViewWithAccessibilityLabel{
    NSString *text = @" ?He?ll?o";
    [tester clearTextFromAndThenEnterText:text intoViewWithAccessibilityLabel:@"Greeting"];
    UITextField * tf = (UITextField*)[tester waitForViewWithAccessibilityLabel:@"Greeting"];
    XCTAssertTrue([tf.text isEqualToString:text]);
}

預期TextField的輸入

- (void)testThatBackspaceDeletesOneCharacter{
    [tester enterText:@"hi\bello" intoViewWithAccessibilityLabel:@"Other Text" traits:UIAccessibilityTraitNone expectedResult:@"hello"];
    [tester waitForViewWithAccessibilityLabel:@"Greeting" value:@"Deleted something." traits:UIAccessibilityTraitNone];
    UIView *textView = [tester waitForViewWithAccessibilityLabel:@"Other Text"];
    XCTAssertEqualObjects([tester textFromView:textView], @"hello");
}

CascadingFailure

失敗用例測試

- (void)testCascadingFailure{
    KIFExpectFailure([system failA]);
    KIFExpectFailureWithCount([system failA], 4);
}

補充Tips

按鈕的title、類的title,可以直接做為訪問標籤
如果UI元件被鍵盤擋住了,需要先退掉鍵盤
如果UI元件不在螢幕範圍內,不可以訪問,但是滾動檢視,可以訪問,且會出現在可視範圍。
無法訪問系統自己的彈窗。例如app想定位使用者,不能自動點選允許;但是app自己的彈窗,可以操作的
類內部的多個測試方法的測試順序,是無序的
類與類的測試順序,是無序的
可以將某個測試類或者測試方法給disable掉

最後

到目前為止,我們應該對KIF的使用性有很好的瞭解,腦子裡也應該有不少主意,大概瞭解如何利用這個高效的功能測試工具來測試你自己的應用程式。由於KIF測試用例是OCUnit的子類,並在標準的Xcode5測試框架下執行,你可以使用持續整合來跑這些測試。當你幹別的事情的時候,你擁有了一個能夠像人的手指一樣觸控的機器人去測試你的應用程式。

參考文獻:

推薦閱讀

相關文章