iOS - UnitTests 單元測試


1、UnitTests

  • 在計算機編程中,單元測試(又稱為模塊測試, Unit Testing)是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對於面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。

  • 通常來說,程序員每修改一次代碼就會修改某個單元,那我們就可以對這個單元做修改的驗證(單元測試),在編寫程序的過程中前后很可能要進行多次單元測試,以證實程序達到軟件規格書(產品需求)要求的工作目標,而且沒有程序錯誤。雖然單元測試不是什么必須的,但也不壞,這牽涉到項目管理的政策決定。

  • 單元測試可以方便測試一些功能是否正常運行,調試接口是否能正常使用。有時候你可能是為了測試某一個網絡接口,然后每次都重新啟動並且經過很多操作之后才測試到了那個網絡接口,如果使用了單元測試,就可以直接測試那個方法,相對方便很多。比如由於修改較多,我們想測試一下分享功能是否正常,這時候就有用了,而不是重新啟動程序,進入到分享界面,點擊分享,填寫分享內容。其實單元測試並沒有降低我們打代碼的效率,我們可以在單元測試通過了,直接用到相應的地方。

  • 當然單元測試也有一些高級的作用,比如自動發布、自動測試(特別在一些大的項目,以防止程序被誤改或引起新的問題)。

1.1 iOS 中的單元測試框架

  • XCTest 是蘋果自帶的測試框架。

  • GHUnit 是一個可視化的測試框架,有了它,你可以點擊 APP 來決定測試哪個方法,並且可以點擊查看測試結果等。

  • OCMock 是模擬某個方法或者屬性的返回值,你可能會疑惑為什么要這樣做?使用模型生成的模型對象,再傳進去不就可以了?答案是可以的,但是有特殊的情況。比如你測試的是方法 A,方法 A 里面調用到了方法 B,而且方法 B 是有參數傳入,但又不是方法 A 所提供。這時候,你可以使用 OCMock 來模擬方法 B 返回的值。在不影響測試的情況下,就可以這樣去模擬。除了這些,在沒有網絡的情況下,也可以通過 OCMock 模擬返回的數據。

  • UITests 是通過代碼化來實現自動點擊界面,輸入文字等功能。靠人工操作的方式來覆蓋所有測試用例是非常困難的,尤其是加入新功能以后,舊的功能也要重新測試一遍,這導致了測試需要花非常多的時間來進行回歸測試,這里產生了大量重復的工作,而這些重復的工作有些是可以自動完成的,這時候 UITests 就可以幫助解決這個問題了。

1.2 單元測試思路

  • 1、單元測試是以代碼測試代碼。不是靠 NSLog 來測試,NSLog 是程序員用眼睛看的笨辦法。而是使用斷言來測試的,提前預判條件必須滿足。

    	XCTAssert(expression, ...)
    	XCTAssert(條件, 不滿足條件的描述)
    
  • 2、單元測試與應用程序開發屬於共存關系,而非嵌入關系,所以必須創建一個單獨的測試目標。

  • 3、可以在單元測試類中編寫單獨的測試用例方法。這些方法與普通的方法類似,但是方法名稱必須以 test 開頭,且不能有參數,不然不會識別為測試方法。

  • 4、測試方法可以直接寫在 - (void)testExample 中,或者寫在以 test 開頭的測試用例方法中。

  • 5、單元測試需要在真機上進行,為了能夠在設備中真實地運行應用程序用例,需要安裝開發配置文件(development provision file)。

  • 6、需要注意,在應用程序上運行單元測試用例並不是一個交互過程,所有的運行控制(包括提供值)都由測試用例自身掌握。

  • 7、不是所有的方法都需要測試。例如私有方法不需要測試,只有暴露在 .h 中的方法需要測試。

  • 8、一般而言,代碼的覆蓋度大概在 50% ~ 70%。從 github 上得知:YYModel 測試覆蓋度為 83%,AFNetworking 測試覆蓋度為 77%,兩者都是比較高的。

2、XCTest 單元測試

2.1 測試使用方法

  • 單元調試操作,兩種方法,按快捷鍵 Command + U 進行單元測試,這個快捷鍵是全部測試。

    UnitTests1

  • 調試可以在斷言處調試,也可以在函數部分調試。錯誤提示是在斷言處顯示,不會在平台展示。

    UnitTests2

2.2 測試類中的方法

  • 測試方法

    	- (void)setUp {
    	    [super setUp];
    	    // Put setup code here. This method is called before the invocation of each test method in the class.
    	    
    	    // 初始化的代碼,在測試方法調用之前調用
    	}
    	
    	- (void)tearDown {
    	    // Put teardown code here. This method is called after the invocation of each test method in the class.
    	    
    	    // 釋放測試用例的資源代碼,這個方法會每個測試用例執行后調用
    	    
    	    [super tearDown];
    	}
    	
    	- (void)testExample {
    	    // This is an example of a functional test case.
    	    // Use XCTAssert and related functions to verify your tests produce the correct results.
    	    
    	    // 測試用例的方法
    	}
    	
    	- (void)testPerformanceExample {
    	    // This is an example of a performance test case.
    	    
    	    // 測試性能的方法,有 Instrument 調試工具之后,其實這個沒毛用
    	    
    	    [self measureBlock:^{
    	        // Put the code you want to measure the time of here.
    	        
    	        // 需要測試性能的代碼
    	    }];
    	}
    

2.3 測試函數

  • 測試函數

    	// 生成一個失敗的測試
    	XCTFail(format…);
    	
    	// 為空判斷
    	
    		// 為空判斷,a1 為空時通過,反之不通過
    		XCTAssertNil(a1, format...);
    		
    		// 不為空判斷,a1 不為空時通過,反之不通過
    		XCTAssertNotNil(a1, format…);
    	
    	// 為真判斷
    	
    		// 為真判斷,當 expression 求值為 True 時通過
    		XCTAssert(expression, format...);
    		
    		// 為真判斷,當 expression 求值為 True 時通過
    		XCTAssertTrue(expression, format...);
    		
    		// 為假判斷,當 expression 求值為 False 時通過
    		XCTAssertFalse(expression, format...);
    	
    	// 相等判斷
    	
    		// 相等判斷,[a1 isEqual:a2] 值為 True 時通過,其中一個不為空時,不通過
    		XCTAssertEqualObjects(a1, a2, format...);
    		
    		// 不等判斷,[a1 isEqual:a2] 值為 False 時通過
    		XCTAssertNotEqualObjects(a1, a2, format...);
    		
    		// 相等判斷,當 a1 和 a2 是 C 語言標量、結構體或聯合體時使用,a1 == a2 值為 True 時通過
    		XCTAssertEqual(a1, a2, format...);
    		
    		// 不等判斷,當 a1 和 a2 是 C 語言標量、結構體或聯合體時使用
    		XCTAssertNotEqual(a1, a2, format...);
    		
    		// 相等判斷,double 或 float 類型,提供一個誤差范圍,當在誤差范圍(+/-accuracy)以內相等時通過測試
    		XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...);
    		
    		// 不等判斷,double 或 float類型,提供一個誤差范圍,當在誤差范圍以內不等時通過測試
    		XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...);
    	
    	// 異常判斷
    	
    		// 異常判斷,當 expression 發生異常時通過,反之不通過
    		XCTAssertThrows(expression, format...);
    		
    		// 異常判斷,當 expression 發生 specificException 異常時通過,反之發生其他異常或不發生異常均不通過
    		XCTAssertThrowsSpecific(expression, specificException, format...);
    		
    		// 異常判斷,當 expression 發生具體異常、具體異常名稱的異常時通過測試,反之不通過
    		XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...);
    		
    		// 異常判斷,當 expression 沒有發生異常時通過測試
    		XCTAssertNoThrow(expression, format…);
    		
    		// 異常判斷,當 expression 沒有發生具體異常、具體異常名稱的異常時通過測試,反之不通過
    		XCTAssertNoThrowSpecific(expression, specificException, format...);
    		
    		// 異常判斷,當 expression 沒有發生具體異常、具體異常名稱的異常時通過測試,反之不通過
    		XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...);
    

2.4 測試的基本使用

  • 基本使用

    	- (void)testExample {
    	
    	    NSLog(@"自定義測試 testExample");
    	    int a = 3;
    	    XCTAssertTrue(a == 0, "a 不能等於 0");
    	}
    
  • 點擊播放按鈕,開始單個方法的測試

    UnitTests6

  • 出現如下結果,由於我們斷言 a 是等於 0 的,而 a 等於 3,所以測試沒有通過。

    UnitTests7

2.5 測試問題解決

  • 問題描述:fatal error: 'XCTest/XCTest.h' file not found

    • 解決方法

      	在報錯的 Target 中的 Building settings 中 FRAMEWORK_SEARCH_PATHS* 添加
      	
      	$(PLATFORM_DIR)/Developer/Library/Frameworks
      

2.6 單例測試

  • 單例要在並發條件下測試

    	// 測試是否為單例
    	- (void)testAudioManagerSingle {
    	    
    	    // 要在並發條件下測試
    	    
    	    NSMutableArray *managers = [NSMutableArray array];
    	    
    	    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    	        QAudioManager *tempManager = [[QAudioManager alloc] init];
    	        [managers addObject:tempManager];
    	    });
    	    
    	    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    	        QAudioManager *tempManager = [[QAudioManager alloc] init];
    	        [managers addObject:tempManager];
    	    });
    	    
    	    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    	        QAudioManager *tempManager = [QAudioManager defaultManager];
    	        [managers addObject:tempManager];
    	    });
    	    
    	    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    	        QAudioManager *tempManager = [QAudioManager defaultManager];
    	        [managers addObject:tempManager];
    	    });
    	    
    	    QAudioManager *managerOne = [QAudioManager defaultManager];
    	    
    	    // 這里是判斷數組中的對象是否一致
    	    [managers enumerateObjectsUsingBlock:^(QAudioManager *obj, NSUInteger idx, BOOL * _Nonnull stop) {
    	        
    	        XCTAssertEqualObjects(managerOne, obj, @"QAudioManager is single");
    	        
    	        XCTAssertNotEqualObjects(managerOne, obj, @"QAudioManager is not single");
    	    }];
    	}
    

    UnitTests4

2.7 性能測試

  • 性能測試

    • 測試一段代碼(函數/方法)的執行時間,我們通常是用到 CFAbsoluteTimeGetCurrent() 或者 CACurrentMediaTime() 函數,通過差值來計算出時間間隔。
    	+ (instancetype)personWithDict:(NSDictionary *)dic {
    	    
    	    NSString *str1;
    	    for (NSString *str in dic) {
    	        str1 = [str stringByAppendingString:str];
    	    }
    	    str1 = nil;
    	    Person *one = [[self alloc] init];
    	    return one;
    	}
    	
    	- (void)testPerformanceExample {
    	    // This is an example of a performance test case.
    	    
    	    // 測試性能的方法,有 Instrument 調試工具之后,其實這個沒毛用
    	    
    	    [self measureBlock:^{
    	        // Put the code you want to measure the time of here.
    	        
    	        // 需要測試性能的代碼
    	        
    	        NSTimeInterval start = CACurrentMediaTime();
    	        
    	        // 測試用例,循環10000次,為了演示效果
    	        for (NSInteger i = 0; i < 10000; i++) {
    	            [Person personWithDict:@{@"name":@"zhang", @"age":@20}];
    	        }
    	        
    	        // 傳統測試代碼耗時方法
    	        NSLog(@"%lf, 我是香蕉大大", CACurrentMediaTime() - start);
    	        
    	    }];
    	}
    

    UnitTests5

2.8 邏輯測試

  • 邏輯測試

    	// 邏輯測試
    	- (void)testNewPerson {
    	    
    	    // 1.測試 name 和 age 是否一致
    	    [self checkPersonWithDict:@{@"name":@"zhou", @"age":@30}];
    	    
    	    /** 2.測試出 age 不符合實際,那么需要在字典轉模型方法中對 age 加以判斷:
    	     if (obj.age <= 0 || obj.age >= 130) {
    	        obj.age = 0;
    	     }
    	     */
    	    [self checkPersonWithDict:@{@"name":@"zhang", @"age":@200}];
    	    
    	    // 3.測試出 name 為 nil 的情況,因此在 XCTAssert 里添加條件:“person.name == nil“
    	    [self checkPersonWithDict:@{}];
    	    
    	    // 4.測試出 Person 類中沒有 title 這個 key,在字典轉模型方法中實現:- (void)setValue:(id)value forUndefinedKey:(NSString *)key {}
    	    [self checkPersonWithDict:@{@"name":@"zhou", @"age":@30, @"title":@"boss"}];
    	    
    	    // 5.總體再驗證一遍,結果 Build Succeeded,測試全部通過
    	    [self checkPersonWithDict:@{@"name":@"zhou", @"age":@-1, @"title":@"boss"}];
    	}
    	
    	// 根據字典檢查新建的 person 信息
    	- (void)checkPersonWithDict:(NSDictionary *)dict {
    	    
    	    Person *person = [Person personWithDict:dict];
    	    
    	    NSLog(@"%@",person);
    	    
    	    // 獲取字典中的信息
    	    NSString *name = dict[@"name"];
    	    NSInteger age = [dict[@"age"] integerValue];
    	    
    	    // 1.檢查名字
    	    XCTAssert([name isEqualToString:person.name] || person.name == nil, @"姓名不一致");
    	    
    	    // 2.檢查年齡
    	    if (person.age.integerValue > 0 && person.age.integerValue < 130) {
    	        XCTAssert(age == person.age.integerValue, @"年齡不一致");
    	    } else {
    	        XCTAssert(person.age == 0, @"年齡超限");
    	    }
    	}
    

2.9 網絡請求測試

  • 安裝 AFNetworking 和 STAlertView

  • 由於測試方法主線程執行完就會結束,所以需要設置一下,否則沒法查看異步返回結果。在方法結束前設置等待,調回回來的時候再讓它繼續執行。

    	//waitForExpectationsWithTimeout是等待時間,超過了就不再等待往下執行。
    	#define WAIT do {\
    	[self expectationForNotification:@"RSBaseTest" object:nil handler:nil];\
    		[self waitForExpectationsWithTimeout:30 handler:nil];\
    	} while (0);
    	
    	#define NOTIFY \
    	[[NSNotificationCenter defaultCenter]postNotificationName:@"RSBaseTest" object:nil];
    
  • 增加測試方法 testRequest

    	- (void)testRequest{
    	    // 獲得請求管理者
    	    AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
    	    mgr.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/html",nil];
    	
    	    // 發送 GET 請求
    	    [mgr GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    	        XCTAssertNotNil(responseObject, @"返回出錯");
    	        NOTIFY // 繼續執行
    	    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    	        XCTAssertNil(error, @"請求出錯");
    	        NOTIFY // 繼續執行
    	    }];
    	    WAIT  //暫停
    	}
    
  • 有時候我們想測試一下整個流程是否可以跑通,比如獲取驗證碼、登錄、上傳頭像,查詢個人資料。其實只要輸入驗證碼就可以完成整個測試。這時候就需要用到輸入框了,以便程序繼續執行。使用了一個第三方的彈出輸入框 STAlertView,前面已經設置。

    	self.stAlertView = [[STAlertView alloc] initWithTitle:@"驗證碼" 
    	                                              message:nil 
    	                                        textFieldHint:@"請輸入手機驗證碼" 
    	                                       textFieldValue:nil 
    	                                    cancelButtonTitle:@"取消" 
    	                                     otherButtonTitle:@"確定" 
    	                                    cancelButtonBlock:^{
    	    // 點擊取消返回后執行
    	    [self testAlertViewCancel];
    	    NOTIFY 	// 繼續執行
    	} otherButtonBlock:^(NSString *b) {
    	    // 點擊確定后執行
    	    [self alertViewComfirm:b];
    	     NOTIFY 	// 繼續執行
    	}];
    	
    	[self.stAlertView show];
    

2.10 高級自動化單元測試

3、OCMock 單元測試

3.1 OCMock

  • Mock 測試

    • Mock 測試是個很神奇而又很酷的技術,在測試過程中,對於一些不容易構造或不容易獲取的對象,此時你可以創建一個虛擬的對象(mock object)來完成測試。

    • 例如你可能要嘗試 100 次才會返回一個 NSError,通過 mock object 你可以自行創建一個 NSError 對象,測試在出錯情況下程序的處理是否符合你的預期。

    • 例如你要連接服務器但是服務器在實驗室,你在外工作的時候就無法測試了,這個時候你可以創建一個虛擬的服務器,並返回一些你指定的數據,從而繞過服務器。

    • 例如假設你要訪問一個數據庫,但是訪問過程的開銷巨大,這時你可以虛擬一個數據庫,並且返回一些自行定制的數據,從而繞過了數據庫的訪問。

    • Mock 的思想很簡單:沒有條件?我們就自行創造條件。

  • OCMock

    • OCMock 是一個用於為 iOS 或 macOS 項目配置 Mock 測試的開源項目,如果目標是 iOS 項目那么生成的是靜態庫,如果是 macOS 項目生成的是框架。OCMock 其實現思想就是根據要 mock 的對象的 class 來創建一個對應的對象,並且設置好該對象的屬性和調用預定方法后的動作(例如返回一個值,調用代碼塊,發送消息等等),然后將其記錄到一個數組中,接下來開發者主動調用該方法,最后做一個 verify(驗證),從而判斷該方法是否被調用,或者調用過程中是否拋出異常等。

    • OCMock 官網

    • iOS Project Setup:在 iOS 項目中配置 OCMock 的教程

    • erikdoe / ocmock:在 GitHub 上的示例項目,可以參考下其中的一些配置參數

    • OCMock Download:OCMock 的靜態庫、框架和工程文件(可以在這里看 OCMock 的源碼實現)下載地址,已經打包成 dmg 格式了。

3.2 配置 OCMock

  • 1、下載 OCMock Download 的 dmg 文件,將 iOS library 文件夾中的文件(libOCMock.a 和 OCMock 文件夾)拷貝到要測試的項目根目錄下。打開工程,將拷貝的文件添加到項目工程中。

    UnitTests8

  • 2、打開 OCMockDemoTests Target 的 Build Phases,添加 libOCMock.a 到要鏈接的類庫中。

    UnitTests9

  • 3、打開 Build Settings,搜索 Other Linker Flags,設置如下

    	-force_load
    	"$(SRCROOT)/OCMock/libOCMock.a"
    	-ObjC
    

    UnitTests10

    • 這里的 -ObjC 表示告訴鏈接器,要把 OC 類和 Category 加載到工程中,但是該設置有 Bug,所以還要用 -all_load 或者 -force_load 來加載靜態庫中沒有加載進來的 Category。如果使用 -all_load 會把所有相關無關的文件都 load 進來,使得目標程序變得更大,所以用 -force_load 來指定要加載的靜態庫就可以了,下面的 "$(SRCROOT)/OCMock/libOCMock.a" 就是靜態庫文件在 Finder 中的路徑。
  • 4、再搜索 Header Search Paths,設置如下

    	"$(SRCROOT)/OCMock"
    

    UnitTests11

    • "$(SRCROOT)/OCMock" 給出的是 OCMock 的頭文件在 Finder 中的路徑,因此該選項告訴編譯器應該到哪里去尋找 OCMock 靜態庫的頭文件。

3.3 編寫 mock 測試

  • 新建一個 test case class 類,基類為 XCTestCase,命名為 MockTableTests。

  • 首先我們測試一下 TableDataSource 的 numberOfRowsInSection 方法是否返回了正確的值,測試代碼如下

    	- (void)testNumberOfRows {
    	    
    	    // 創建 Table View 的 DataSource
    	    TableViewCellConfigureBlock cellConfigureBlock = ^(UITableViewCell *cell, NSString *item) {
    	        cell.textLabel.text = item;
    	    };
    	    
    	    TableDataSource *tableSource = [[TableDataSource alloc] initWithItems:@[@"1", @"2", @"3"]
    	                                                           CellIdentifier:@"foo"
    	                                                       ConfigureCellBlock:cellConfigureBlock];
    	    
    	    // 創建 mock table view
    	    id mockTableView = [OCMockObject mockForClass:[UITableView class]];
    	    
    	    // 斷言
    	    XCTAssertEqual([tableSource tableView:mockTableView numberOfRowsInSection:0], (NSInteger)3, 
    	                   @"Mock table returns a bad number of rows in section 0");
    	}
    
    • 1、首先創建 data source,用於下文中調用 numberOfRowsInSection 方法。注意這里 Table View 中的內容 @[@"1", @"2", @"3"] 是需要我們手動配置的。這里也體現了 mock 的一個局限性,就是 mock object 的關鍵屬性都要我們自己定制,如果要模擬的對象非常的大,那么創建一個 mock object 的成本將遠遠大於單元測試帶來的效益。

    • 2、如果要單獨測試 numberOfRowsInSection 方法,我們就需要有一個 TableView,因此要通過 OCMockObject 的 mockForClass 類方法來創建一個 mock table view。

    • 3、通過 data source 調用方法,並使用斷言判斷。

  • 如果在測試時,我們只想在控制台中看見這個方法的輸出信息,可以點擊方法前面的一個小播放按鈕

    UnitTests12

  • 控制台輸出

    UnitTests13

  • 下面來編寫一個稍微復雜點的 mock 測試,用來測試 UITableViewDataSource 中的 cellForRowAtIndexPath 方法。

    	- (void)testCellConfiguration {
    	    
    	    // 創建 Table data source
    	    __block UITableViewCell *configuredCell = nil;
    	    __block id configuredObject = nil;
    	    TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b) {
    	        configuredCell   = a;
    	        configuredObject = b;
    	    };
    	    TableDataSource *dataSource = [[TableDataSource alloc] initWithItems:@[@"a", @"b"]
    	                                                          CellIdentifier:@"foo"
    	                                                      ConfigureCellBlock:block];
    	    
    	    // 創建 mock table view
    	    id mockTableView = [OCMockObject mockForClass:[UITableView class]];
    	    
    	    // 設定 mock table view 的行為
    	    UITableViewCell *cell = [[UITableViewCell alloc] init];
    	    [[[mockTableView expect] andReturn:cell] dequeueReusableCellWithIdentifier:@"foo"
    	                                                                  forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
    	    // [[[mockTableView stub] andReturn:cell] dequeueReusableCellWithIdentifier:@"foo" forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
    	    
    	    // 主動調用 cellForRowAtIndexPath 方法
    	    id result = [dataSource tableView:mockTableView
    	                cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
    	    
    	    // 驗證 mock table view 的行為
    	    [mockTableView verify];
    	    
    	    // 斷言
    	    XCTAssertEqual(result, cell, @"Should return the dummy cell.");
    	    XCTAssertEqual(configuredCell, cell, @"This should have been passed to the block.");
    	    XCTAssertEqualObjects(configuredObject, @"a", @"This should have been passed to the block.");
    	}
    
    • 1、創建 Table data source,用於下文調用 cellForRowAtIndexPath 方法。

    • 2、創建 mock table view。

    • 3、如果 mock table view 調用了 dequeueReusableCellWithIdentifier:@"foo" forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]] 方法,那么就返回上面已經創建好的 UITableViewCell 對象,expect 方法表示該方法必須被調用(見5.)。

    • 4、通過 Table data source 主動調用 cellForRowAtIndexPath 方法,此時會觸發 mock table view 調用 dequeueReusableCellWithIdentifier:forIndexPath: 方法。

    • 5、最后要調用 verify 方法,用於驗證 mock table view 的行為。如果 mock table view 在某個方法中調用了 expect,那么該方法必須在 verify 之前被調用,否則測試無法通過。如果 mock table view 調用的是 stub,那么 verify 時 OCMock 並不關心該方法是否調用過,只會關心調用過程是否發生異常或有測試被拒絕等。

    • 6、斷言,在這里進行各種比較。

4、GHUnit 單元測試

4.1 GitHub

  • 可能大家都注意到了,在運行測試后,控制台中的輸出可以用慘不忍睹來形容。這時我們可以嘗試另一個工具:GHUnit 框架,這個工具是有 GUI 的。

  • gh-unit / gh-unit:該項目在 GitHub 上的地址。

  • guide_testing Document:編寫測試的參考文檔。

4.2 GHUnit 使用

5、UITests UI 測試

5.1 UITests

  • UITests 是一個自動測試 UI 與交互的 Testing 組件。它可以通過編寫代碼、或者是記錄開發者的操作過程並代碼化,來實現自動點擊某個按鈕、視圖,或者自動輸入文字等功能。

  • 在實際的開發過程中,隨着項目越做越大,功能越來越多,僅僅靠人工操作的方式來覆蓋所有測試用例是非常困難的,尤其是加入新功能以后,舊的功能也要重新測試一遍,這導致了測試需要花非常多的時間來進行回歸測試,這里產生了大量重復的工作,而這些重復的工作有些是可以自動完成的,這時候 UITests 就可以幫助解決這個問題了。

5.2 測試元素語法

  • 1、XCUIApplication

    • 繼承 XCUIElement,這個類掌管應用程序的生命周期,里面包含兩個主要方法

    • launch():啟動程序

    • terminate():終止程序

  • 2、XCUIElement

    • 繼承 NSObject,實現協議 XCUIElementAttributes, XCUIElementTypeQueryProvider

    • 可以表示系統的各種UI元素

  • 3、exist

    • 可以讓你判斷當前的 UI 元素是否存在,如果對一個不存在的元素進行操作,會導致測試組件拋出異常並中斷測試
  • 4、descendantsMatchingType(type:XCUIElementType)->XCUIElementQuery:

    • 取某種類型的元素以及它的子類集合
  • 5、childrenMatchingType(type:XCUIElementType)->XCUIElementQuery:

    • 取某種類型的元素集合,不包含它的子類

    • 這兩個方法的區別在於,你僅使用系統的 UIButton 時,用 childrenMatchingType 就可以了,如果你還希望查詢自己定義的子 Button,就要用 descendantsMatchingType

  • 6、另外 UI 元素還有一些交互方法

    • tap():點擊
    • doubleTap():雙擊
    • pressForDuration(duration: NSTimeInterval):長按一段時間,在你需要進行延時操作時,這個就派上用場了
    • swipeUp():這個響應不了 pan 手勢,暫時沒發現能用在什么地方,也可能是 beta 版的 bug,先不解釋
    • typeText(text: String):用於 textField 和 textView 輸入文本時使用,使用前要確保文本框獲得輸入焦點,可以使用 tap() 函數使其獲得焦點
  • 7、XCUIElementAttributes 協議

    • 里面包含了 UIAccessibility 中的部分屬性

      UnitTests22

    • 可以方便你查看當前元素的特征,其中 identifier 屬性可用於直接讀取元素,不過該屬性在 UITextField 中有 bug,暫時不清楚原因

  • 8、XCUIElementTypeQueryProvider 協議

    • 里面包含了系統中大部分 UI 控件的類型,可通過讀屬性的方式取得某種類型的 UI 集合,部分屬性截圖如下

      UnitTests23

5.3 添加 UITests

  • 1、如果是新項目,則創建工程的時候可以直接勾選選項,如下圖

    UnitTests14

  • 2、如果是已有的項目,可以通過添加 target 的方式添加一個 UI Tests,點擊 xcode 的菜單,找到 target 欄

    UnitTests15

    • 在 Test 選項中選擇 Cocoa Touch UI Testing Bundle

      UnitTests16

  • 3、這時候 test 組件添加成功,它在項目中的位置如下圖所示

    UnitTests17

5.4 創建測試代碼

  • 1、手動創建測試代碼

    • 打開測試文件,在 testExample() 方法中添加測試代碼,如果不知道如何寫測試代碼,則可以參考自動生成的代碼樣式。

      UnitTests18

  • 2、自動生成測試步驟

    • 選擇測試文件后,點擊錄制按鈕。

      UnitTests19

    • 這時候開始進行操作,它會記錄你的操作步驟,並生成測試代碼,下圖就是在一些操作后自動生成的測試代碼。

      UnitTests20

    • 這時候可以分析測試代碼的語法,以便你自己手動修改或者手寫測試代碼。

  • 3、開始測試

    • 點擊 testExample 方法旁邊的播放按鈕,它就開始進行自動測試了,這時候你會看到 App 在自動操作。

      UnitTests21


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM