寫好測試,提高應用質量

相信在國內一些中小型公司,開發者不多會去寫軟件測試相關的代碼。固然這背後有一些緣由在。本文就講講 iOS 開發中的軟件測試相關的內容。c++

1、 測試的重要性

測試很重要!測試很重要!測試很重要!重要的事情說三遍。git

場景1:每次咱們寫完代碼後都須要編譯運行,以查看應用程序的表現是否符合預期。假如改動點、代碼量小,那驗證成本低一些,假如不符合預期,則說明咱們的代碼有問,人工去排查問題花費的時間也少一些。假如改動點不少、受影響的地方較多,咱們首先要大概猜想受影響的功能,而後去定位問題、排查問題的成本就很高。github

場景2:你新接手的 SDK 某個子功能須要作一次技術重構。可是你只有在公司內部的代碼託管平臺上能夠看到一些 Readme、接入文檔、系統設計文檔、技術方案評估文檔等一堆文檔。可能你會看完再去動手重構。當你重構完了,找了公司某條業務線的 App 接入測試,點了幾下發現發生了奔潰。😂 心想,本地測試、debug 都正常但是爲何接入後就 Crash 了。其實想一想也好理解,你本地重構只是確保了你開發的那個功能運行正常,你很難確保你寫的代碼沒有影響其餘類、其餘功能。假如以前的 SDK 針對每一個類都有單元測試代碼,那你在新功能開發完畢後完整跑一次單元測試代碼就行了,保證每一個 Unit Test 都經過、分支覆蓋率達到約定的線,那麼基本上是沒問題的。objective-c

場景3:在版本迭代的時候,計劃功能 A,從開發、聯調、測試、上線共2周時間。老司機作事很自信,這麼簡單的 UI、動畫、交互,代碼風騷,參考服務端的「領域驅動」在該 feature 開發階段落地試驗了下。聯調、本地測試都經過了,還剩3天時間,本覺得測試1天,bug fix 一天,最後一天提交審覈。代碼跟你開了個玩笑,測試完 n 個 bug(大大超出預期)。爲了避免影響 App 的發佈上架,不得不熬夜修 bug。將全部的測試都經過測試工程師去處理,這個階段理論上質量應該很穩定,否則該階段發現代碼異常、技術設計有漏洞就來不及了,你須要協調各個團隊的資源(可能接口要改動、產品側要改動),這個階段形成改動的成本很是大。數據庫

相信大多數開發者都遇到過上述場景的問題。其實上述這幾個問題都有解,那就是「軟件測試」。express

2、軟件測試

1. 分類

軟件測試就是在規定的條件下對應用程序進行操做,以發現程序錯誤,衡量軟件質量,並對其是否能知足設計要求進行評估的過程。編程

合理應用軟件測試技術,就能夠規避掉第一部分的3個場景下的問題。json

軟件測試強調開發、測試同步進行,甚至是測試先行,從需求評審階段就先考慮好軟件測試方案,隨後才進行技術方案評審、開發編碼、單元測試、集成測試、系統測試、迴歸測試、驗收測試等。安全

軟件測試從測試範圍分爲:單元測試、集成測試、系統測試、迴歸測試、驗收測試(有些公司會談到「冒煙測試「,這個詞的精肯定義不知道,可是學軟件測試課的時候按照範圍就只有上述幾個分類)。工程師本身負責的是單元測試。測試工程師、QA 負責的是集成測試、系統測試。網絡

單元測試(Unit Testing):又稱爲模塊測試,是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工做。「單元」的概念會比較抽象,它不只僅是咱們所編寫的某個方法、函數,也多是某個類、對象等。

軟件測試從開發模式分爲:面向測試驅動開發 TDD (Test-driven development)、面向行爲驅動開發 BDD (Behavior-driven development)。

2. TDD

TDD 的思想是:先編寫測試用例,再快速開發代碼,而後在測試用例的保證下,能夠方便安全地進行代碼重構,提高應用程序的質量。一言以蔽之就是經過測試來推進開發的進行。正是因爲這個特色,TDD 被普遍使用於敏捷開發。

也就是說 TDD 模式下,首先考慮如何針對功能進行測試,而後去編寫代碼實現,再不斷迭代,在測試用例的保證下,不斷進行代碼優化。

優勢:目標明確、架構分層清晰。可保證開發代碼不會偏離需求。每一個階段持續測試

缺點:技術方案須要先評審結束、架構須要提早搭建好。假如需求變更,則前面步驟須要從新執行,靈活性較差。

3. BDD

BDD 即行爲驅動開發,是敏捷開發技術之一,經過天然語言定義系統行爲,以功能使用者的角度,編寫需求場景,且這些行爲描述能夠直接造成需求文檔,同時也是測試標準。

BDD 的思想是跳出單一的函數,針對的是行爲而展開的測試。BDD 關心的是業務領域、行爲方式,而不是具體的函數、方法,經過對行爲的描述來驗證功能的可用性。BDD 使用 DSL (Domin Specific Language)領域特定語言來描述測試用例,這樣編寫的測試用例很是易讀,看起來跟文檔同樣易讀,BDD 的代碼結構是 Given->When->Then

優勢:各團隊的成員能夠集中在一塊兒,設計基於行爲的計測試用例。

4. 對比

根據特色也就是找到了各自的使用場景,TDD 主要針對開發中的最小單元進行測試,適合單元測試。而 BDD 針對的是行爲,因此測試範圍能夠再大一些,在集成測試、系統測試中均可以使用

TDD 編寫的測試用例通常針對的是開發中的最小單元(好比某個類、函數、方法)而展開,適合單元測試。

BDD 編寫的測試用例針對的是行爲,測試範圍更大一些,適合集成測試、系統測試階段。

3、 單元測試編碼規範<a name="codeRules"></a>

本文的主要重點是針對平常開發階段工程師能夠作的事情,也就是單元測試而展開。

編寫功能、業務代碼的時候通常會遵循 kiss 原則 ,因此類、方法、函數每每不會太大,分層設計越好、職責越單1、耦合度越低的代碼越適合作單元測試,單元測試也倒逼開發過程當中代碼分層、解耦。

可能某個功能的實現代碼有30行,測試代碼有50行。單元測試的代碼如何編寫才更合理、整潔、規範呢?

1. 編碼分模塊展開

先貼一段代碼。

-  (void)testInsertDataInOneSpecifiedTable
{
    XCTestExpectation *exception = [self expectationWithDescription:@"測試數據庫插入功能"];
    // given
    [dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta];
    NSMutableArray *insertModels = [NSMutableArray array];
    for (NSInteger index = 1; index <= 10000; index++) {
        HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
        model.log_id = index;
				// ...
        [insertModels addObject:model];
    }
    // when
    [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
 	  // then 
  	[dbInstance recordsCountInTableType:HCTLogTableTypeMeta completion:^(NSInteger count) {
        XCTAssert(count == insertModels.count, @"「數據增長」功能:異常");
        [exception fulfill];
    }];
    [self waitForExpectationsWithCommonTimeout];
}

能夠看到這個方法的名稱爲 testInsertDataInOneSpecifiedTable,這段代碼作的事情經過函數名能夠看出來:測試插入數據到某個特定的表。這個測試用例分爲3部分:測試環境所需的先決條件準備;調用所要測試的某個方法、函數;驗證輸出和行爲是否符合預期。

其實,每一個測試用例的編寫也要按照該種方式去組織代碼。步驟分爲3個階段:Given->When->Then。

因此單元測試的代碼規範也就出來了。此外單元測試代碼規範統一後,每一個人的測試代碼都按照這個標準展開,那其餘人的閱讀起來就更加容易、方便。按照這3個步驟去閱讀、理解測試代碼,就能夠清晰明瞭的知道在作什麼。

2. 一個測試用例只測試一個分支

咱們寫的代碼有不少語句組成,有各類邏輯判斷、分支(if...else、swicth)等等,所以一個程序從一個單一入口進去,過程可能產生 n 個不一樣的分支,可是程序的出口老是一個。因此因爲這樣的特性,咱們的測試也須要針對這樣的現狀走完儘量多的分支。相應的指標叫作「分支覆蓋率」。

假如某個方法內部有 if...else...,咱們在測試的時候儘可能將每種狀況寫成一個單獨的測試用例,單獨的輸入、輸出,判斷是否符合預期。這樣每一個 case 都單一的測試某個分支,可讀性也很高。

好比對下面的函數作單元測試,測試用例設計以下

- (void)shouldIEatSomething
{
   BOOL shouldEat = [self getAteWeight] < self.dailyFoodSupport;
   if (shouldEat) {
     [self eatSomemuchFood];
   } else {
     [self doSomeExercise];
   }
}
- (void)testShouldIEatSomethingWhenHungry
{
   // ....
}

- (void)testShouldIEatSomethingWhenFull
{
  // ...
}

3. 明確標識被測試類

這條主要站在團隊合做和代碼可讀性角度出發來講明。寫過單元測試的人都知道,可能某個函數原本就10行代碼,但是爲了測試它,測試代碼寫了30行。一個方法這樣寫問題不大,多看看就看明白是在測試哪一個類的哪一個方法。但是當這個類自己就很大,測試代碼很大的狀況下,無論是做者自身仍是多年後負責維護的其餘同事,看這個代碼閱讀成本會很大,須要先看測試文件名 代碼類名 + Test 才知道是測試的是哪一個類,看測試方法名 test + 方法名 才知道是測試的是哪一個方法。

這樣的代碼可讀性不好,因此應該爲當前的測試對象特殊標記,這樣測試代碼可讀性越強、閱讀成本越低。好比定義局部變量 _sut 用來標記當前被測試類(sut,System under Test,軟件測試領域有個詞叫作被測系統,用來表示正在被測試的系統)。

#import <XCTest/XCTest.h>
#import "HCTLogPayloadModel.h"

@interface HCTLogPayloadModelTest : HCTTestCase
{
    HCTLogPayloadModel *_sut;
}

@end

@implementation HCTLogPayloadModelTest

- (void)setUp
{
    [super setUp];
    HCTLogPayloadModel *model = [[HCTLogPayloadModel alloc] init];
    model.log_id = 1;
    // ...
    _sut = model;
}

- (void)tearDown
{
    _sut = nil;
    [super tearDown];
}

- (void)testGetDictionary
{
    NSDictionary *payloadDictionary = [_sut getDictionary];
    XCTAssert([(NSString *)payloadDictionary[@"report_id"] isEqualToString:@"001"] &&
              [payloadDictionary[@"size"] integerValue] == 102 &&
              [(NSString *)payloadDictionary[@"meta"] containsString:@"meiying"],
              @"HCTLogPayloadModel 的 「getDictionary」功能異常");
}

@end

4. 使用分類來暴露私有方法、私有變量

某些場景下寫的測試方法內部可能須要調用被測對象的私有方法,也可能須要訪問被測對象的某個私有屬性。可是測試類裏面是訪問不到被測類的私有屬性和私有方法的,藉助於 Category 能夠實現這樣的需求。

爲測試類添加一個分類,後綴名爲 UnitTest。以下所示

HermesClient 類有私有屬性 @property (nonatomic, strong) NSString *name;,私有方法 - (void)hello。爲了在測試用例中訪問私有屬性和私有方法,寫了以下分類

// HermesClientTest.m

@interface HermesClient (UnitTest)

- (NSString *)name;

- (void)hello;

@end
  
@implementation HermesClientTest

- (void)testPrivatePropertyAndMethod
{
    NSLog(@"%@",[HermesClient sharedInstance].name);
    [[HermesClient sharedInstance] hello];
}
@end

4、 單元測試下開發模式、技術框架選擇

單元測試是按照測試範圍來劃分的。TDD、BDD 是按照開發模式來劃分的。所以就有各類排列組合,這裏咱們只關心單元測試下的 TDD、BDD 方案。

在單元測試階段,TDD 和 BDD 均可以適用。

1. TDD

TDD 強調不斷的測試推進代碼的開發,這樣簡化了代碼,保證了代碼質量。

思想是在拿到一個新的功能時,首先思考該功能如何測試,各類測試用例、各類邊界 case;而後完成測試代碼的開發;最後編寫相應的代碼以知足、經過這些測試用例。

TDD 開發過程相似下圖:

<a name="TDDStructure"></a>

  • 先編寫該功能的測試用例,實現測試代碼。這時候去跑測試,是不經過的,也就是到了紅色的狀態
  • 而後編寫真正的功能實現代碼。這時候去跑測試,測試經過,也就是到了綠色的狀態
  • 在測試用例的保證下,能夠重構、優化代碼

拋出一個問題:TDD 看上去很好,應該用它嗎?

這個問題不用着急回答,回答了也不會有對錯之分。開發中常常是這樣一個流程,新的需求出來後,先通過技術評審會議,肯定宏觀層面的技術方案、肯定各個端的技術實現、使用的技術等,整理出開發文檔、會議文檔。工期評估後開始編碼。事情這麼簡單嗎?前期即便想的再充分、再細緻,可能仍是存在特殊 case 漏掉的狀況,致使技術方案或者是技術實現的改變。若是採用 TDD,那麼以前新功能給到後,就要考慮測試用例的設計、編寫了測試代碼,在測試用例的保證下再去實現功能。若是遇到了技術方案的變動,以前的測試用例要改變、測試代碼實現要改變。可能新增的某個 case 致使大部分的測試代碼和實現代碼都要改變。

如何開展 TDD**

  1. 新建一個工程,確保 「Include Unit Tests」 選項是選中的狀態

    TDD Step 1

  2. 建立後的工程目錄以下

    TDD step2

  3. 刪除 Xcode 建立的測試模版文件 TDDDemoTests.m

  4. 假如咱們須要設計一我的類,它具備吃飯的功能,且當他吃完後會說一句「好飽啊」。

  5. 那麼按照 TDD 咱們先設計測試用例。假設有個 Person 類,有個對象方法叫作吃飯,吃完飯後會返回一個「好飽啊」的字符串。那測試用例就是

    步驟 指望 結果
    實例化 Person 對象,調用對象的 eat 方法 調用後返回「好飽啊」
  6. 實現測試用例代碼。建立繼承自 Unit Test Case class 的測試類,命名爲 工程前綴+測試類名+Test,也就是 TDDPersonTest.m

    TDD step 3

  7. 由於要測試 Person 類,因此在主工程中建立 Person 類

  8. 由於要測試人類在吃飯後說一句「好飽啊」。因此設想那個類目前只有一個吃飯的方法。因而在 TDDPersonTest.m 中建立一個測試函數 -(void)testReturnStatusStringWhenPersonAte;函數內容以下

    - (void)testReturnStatusStringWhenPersonAte
    {
        // Given
        Person *somebody = [[Person alloc] init];
    
        // When
        NSString *statusMessage = [somebody performSelector:@selector(eat)];
    
        // Then
        XCTAssert([statusMessage isEqualToString:@"好飽啊"], @"Person 「吃飯後返回「好飽啊」」功能異常");
    }
  9. Xcode 下按快捷鍵 Command + U,跑測試代碼發現是失敗的。由於咱們的 Person 類根本沒實現相應的方法

  10. TDD 開發過程能夠看到,咱們如今是紅色的 「Fail」 狀態。因此須要去 Person 類中實現功能代碼。Person 類以下

    #import "Person.h"
    
    @implementation Person
    
    - (NSString *)eat
    {
        [NSThread sleepForTimeInterval:1];
        return @"好飽啊";;
    }
    
    @end
  11. 再次運行,跑一下測試用例(Command + U 快捷鍵)。發現測試經過,也就是TDD 開發過程中的綠色 「Success」 狀態。

  12. 例子比較簡單,假如狀況須要,能夠在 -(void)setUp 方法裏面作一些測試的前置準備工做,在 -(void)tearDown 方法裏作資源釋放的操做

  13. 假如 eat 方法實現的不夠漂亮。如今在測試用例的保證下,大膽重構,最後確保全部的 Unit Test case 經過便可。

2. BDD

相比 TDD,BDD 關注的是行爲方式的設計,拿上述「人吃飯」舉例說明。

和 TDD 相比第1~4步驟相同。

  1. BDD 則須要先實現功能代碼。建立 Person 類,實現 -(void)eat;方法。代碼和上面的相同

  2. BDD 須要引入好用的框架 Kiwi,使用 Pod 的方式引入

  3. 由於要測試人類在吃飯後說一句「好飽啊」。因此設想那個類目前只有一個吃飯的方法。因而在 TDDPersonTest.m 中建立一個測試函數 -(void)testReturnStatusStringWhenPersonAte;函數內容以下

    #import "kiwi.h"
    #import "Person.h"
    
    SPEC_BEGIN(BDDPersonTest)
    
    describe(@"Person", ^{
        context(@"when someone ate", ^{
            it(@"should get a string",^{
              	Person *someone = [[Person alloc] init];
                NSString *statusMessage = [someone eat];
                [[statusMessage shouldNot] beNil];
                [[statusMessage should] equal:@"好飽啊"];
            });
        });
    });
    
    SPEC_END

3. XCTest

開發步驟

Xcode 自帶的測試系統是 XCTest,使用簡單。開發步驟以下

  • Tests 目錄下爲被測的類建立一個繼承自 XCTestCase 的測試類。

  • 刪除新建的測試代碼模版裏面的無用方法 - (void)testPerformanceExample- (void)testExample

  • 跟普通類同樣,能夠繼承,能夠寫私有屬性、私有方法。因此能夠在新建的類裏面,根據需求寫一些私有屬性等

  • - (void)setUp 方法裏面寫一些初始化、啓動設置相關的代碼。好比測試數據庫功能的時候,寫一些數據庫鏈接池相關代碼

  • 爲被測類裏面的每一個方法寫測試方法。被測類裏面多是 n 個方法,測試類裏面多是 m 個方法(m >= n),根據咱們在第三部分:單元測試編碼規範裏講過的 一個測試用例只測試一個分支,方法內部有 if、switch 語句時,須要爲每一個分支寫測試用例

  • 爲測試類每一個方法寫的測試方法有必定的規範。命名必須是 test+被測方法名。函數無參數、無返回值。好比 - (void)testSharedInstance

  • 測試方法裏面的代碼按照 Given->When->Then 的順序展開。測試環境所需的先決條件準備;調用所要測試的某個方法、函數;使用斷言驗證輸出和行爲是否符合預期。

  • - (void)tearDown 方法裏面寫一些釋放掉資源或者關閉的代碼。好比測試數據庫功能的時候,寫一些數據庫鏈接池關閉的代碼

斷言相關宏

/*!
 * @function XCTFail(...)
 * Generates a failure unconditionally.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTFail(...) \
    _XCTPrimitiveFail(self, __VA_ARGS__)

/*!
 * @define XCTAssertNil(expression, ...)
 * Generates a failure when ((\a expression) != nil).
 * @param expression An expression of id type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNil(expression, ...) \
    _XCTPrimitiveAssertNil(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertNotNil(expression, ...)
 * Generates a failure when ((\a expression) == nil).
 * @param expression An expression of id type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotNil(expression, ...) \
    _XCTPrimitiveAssertNotNil(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssert(expression, ...)
 * Generates a failure when ((\a expression) == false).
 * @param expression An expression of boolean type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssert(expression, ...) \
    _XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertTrue(expression, ...)
 * Generates a failure when ((\a expression) == false).
 * @param expression An expression of boolean type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertTrue(expression, ...) \
    _XCTPrimitiveAssertTrue(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertFalse(expression, ...)
 * Generates a failure when ((\a expression) != false).
 * @param expression An expression of boolean type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertFalse(expression, ...) \
    _XCTPrimitiveAssertFalse(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertEqualObjects(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) not equal to (\a expression2)).
 * @param expression1 An expression of id type.
 * @param expression2 An expression of id type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertEqualObjects(expression1, expression2, ...) \
    _XCTPrimitiveAssertEqualObjects(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertNotEqualObjects(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) equal to (\a expression2)).
 * @param expression1 An expression of id type.
 * @param expression2 An expression of id type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotEqualObjects(expression1, expression2, ...) \
    _XCTPrimitiveAssertNotEqualObjects(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertEqual(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) != (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertEqual(expression1, expression2, ...) \
    _XCTPrimitiveAssertEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertNotEqual(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) == (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotEqual(expression1, expression2, ...) \
    _XCTPrimitiveAssertNotEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, ...)
 * Generates a failure when (difference between (\a expression1) and (\a expression2) is > (\a accuracy))).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param accuracy An expression of C scalar type describing the maximum difference between \a expression1 and \a expression2 for these values to be considered equal.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, ...) \
    _XCTPrimitiveAssertEqualWithAccuracy(self, expression1, @#expression1, expression2, @#expression2, accuracy, @#accuracy, __VA_ARGS__)

/*!
 * @define XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, ...)
 * Generates a failure when (difference between (\a expression1) and (\a expression2) is <= (\a accuracy)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param accuracy An expression of C scalar type describing the maximum difference between \a expression1 and \a expression2 for these values to be considered equal.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, ...) \
    _XCTPrimitiveAssertNotEqualWithAccuracy(self, expression1, @#expression1, expression2, @#expression2, accuracy, @#accuracy, __VA_ARGS__)

/*!
 * @define XCTAssertGreaterThan(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) <= (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertGreaterThan(expression1, expression2, ...) \
    _XCTPrimitiveAssertGreaterThan(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertGreaterThanOrEqual(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) < (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertGreaterThanOrEqual(expression1, expression2, ...) \
    _XCTPrimitiveAssertGreaterThanOrEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertLessThan(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) >= (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertLessThan(expression1, expression2, ...) \
    _XCTPrimitiveAssertLessThan(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertLessThanOrEqual(expression1, expression2, ...)
 * Generates a failure when ((\a expression1) > (\a expression2)).
 * @param expression1 An expression of C scalar type.
 * @param expression2 An expression of C scalar type.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertLessThanOrEqual(expression1, expression2, ...) \
    _XCTPrimitiveAssertLessThanOrEqual(self, expression1, @#expression1, expression2, @#expression2, __VA_ARGS__)

/*!
 * @define XCTAssertThrows(expression, ...)
 * Generates a failure when ((\a expression) does not throw).
 * @param expression An expression.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertThrows(expression, ...) \
    _XCTPrimitiveAssertThrows(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertThrowsSpecific(expression, exception_class, ...)
 * Generates a failure when ((\a expression) does not throw \a exception_class).
 * @param expression An expression.
 * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertThrowsSpecific(expression, exception_class, ...) \
    _XCTPrimitiveAssertThrowsSpecific(self, expression, @#expression, exception_class, __VA_ARGS__)

/*!
 * @define XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...)
 * Generates a failure when ((\a expression) does not throw \a exception_class with \a exception_name).
 * @param expression An expression.
 * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
 * @param exception_name The name of the exception.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, ...) \
    _XCTPrimitiveAssertThrowsSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)

/*!
 * @define XCTAssertNoThrow(expression, ...)
 * Generates a failure when ((\a expression) throws).
 * @param expression An expression.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNoThrow(expression, ...) \
    _XCTPrimitiveAssertNoThrow(self, expression, @#expression, __VA_ARGS__)

/*!
 * @define XCTAssertNoThrowSpecific(expression, exception_class, ...)
 * Generates a failure when ((\a expression) throws \a exception_class).
 * @param expression An expression.
 * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNoThrowSpecific(expression, exception_class, ...) \
    _XCTPrimitiveAssertNoThrowSpecific(self, expression, @#expression, exception_class, __VA_ARGS__)

/*!
 * @define XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, ...)
 * Generates a failure when ((\a expression) throws \a exception_class with \a exception_name).
 * @param expression An expression.
 * @param exception_class The class of the exception. Must be NSException, or a subclass of NSException.
 * @param exception_name The name of the exception.
 * @param ... An optional supplementary description of the failure. A literal NSString, optionally with string format specifiers. This parameter can be completely omitted.
*/
#define XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, ...) \
    _XCTPrimitiveAssertNoThrowSpecificNamed(self, expression, @#expression, exception_class, exception_name, __VA_ARGS__)

經驗小結

  1. XCTestCase 類和其餘類同樣,你能夠定義基類,這裏面封裝一些經常使用的方法。

    // HCTTestCase.h
    #import <XCTest/XCTest.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface HCTTestCase : XCTestCase
    
    @property (nonatomic, assign) NSTimeInterval networkTimeout;
    
    
    /**
     用一個默認時間設置異步測試 XCTestExpectation 的超時處理
     */
    - (void)waitForExpectationsWithCommonTimeout;
    
    /**
     用一個默認時間設置異步測試的
    
     @param handler 超時的處理邏輯
     */
    - (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler;
    
    
    /**
     生成 Crash 類型的 meta 數據
    
     @return meta 類型的字典
     */
    - (NSDictionary *)generateCrashMetaDataFromReport;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    // HCTTestCase.m
    #import "HCTTestCase.h"
    #import ...
    
    @implementation HCTTestCase
    
    #pragma mark - life cycle
    
    - (void)setUp
    {
        [super setUp];
        self.networkTimeout = 20.0;
        // 1. 設置平臺信息
        [self setupAppProfile];
        // 2. 設置 Mget 配置
        [[TITrinityInitManager sharedInstance] setup];
        // ....
        // 3. 設置 HermesClient
        [[HermesClient sharedInstance] setup];
    }
    
    - (void)tearDown
    {
        [super tearDown];
    }
    
    
    #pragma mark - public Method
    
    - (void)waitForExpectationsWithCommonTimeout
    {
        [self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
    }
    
    - (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler __nullable)handler
    {
        [self waitForExpectationsWithTimeout:self.networkTimeout handler:handler];
    }
    
    
    - (NSDictionary *)generateCrashMetaDataFromReport
    {
        NSMutableDictionary *metaDictionary = [NSMutableDictionary dictionary];
        NSDate *crashTime = [NSDate date];
        metaDictionary[@"MONITOR_TYPE"] = @"appCrash";
        // ...
        metaDictionary[@"USER_CRASH_DATE"] = @([crashTime timeIntervalSince1970] * 1000);
        return [metaDictionary copy];
    }
    
    
    #pragma mark - private method
    
    - (void)setupAppProfile
    {
        [[CMAppProfile sharedInstance] setMPlatform:@"70"];
        // ... 
    }
    
    @end
  2. 上述說的基本是開發規範相關。測試方法內部若是調用了其餘類的方法,則在測試方法內部必須 Mock 一個外部對象,限制好返回值等。

  3. 在 XCTest 內難以使用 mock 或 stub,這些是測試中很是常見且重要的功能

例子

這裏舉個例子,是測試一個數據庫操做類 HCTDatabase,代碼只放某個方法的測試代碼。

- (void)testRemoveLatestRecordsByCount
{
    XCTestExpectation *exception = [self expectationWithDescription:@"測試數據庫刪除最新數據功能"];
    // 1. 先清空數據表
    [dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta];
    // 2. 再插入一批數據
    NSMutableArray *insertModels = [NSMutableArray array];
    NSMutableArray *reportIDS = [NSMutableArray array];
    
    for (NSInteger index = 1; index <= 100; index++) {
        HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
        model.log_id = index;
        // ...
        if (index > 90 && index <= 100) {
            [reportIDS addObject:model.report_id];
        }
        [insertModels addObject:model];
    }
    [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
    
    // 3. 將早期的數據刪除掉(id > 90 && id <= 100)
    [dbInstance removeLatestRecordsByCount:10 inTableType:HCTLogTableTypeMeta];
    
    // 4. 拿到當前的前10條數據和以前存起來的前10條 id 作比較。再判斷當前表中的總記錄條數是否等於 90
    [dbInstance getLatestRecoreds:10 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) {
        NSArray<HCTLogModel *> *latestRTentRecords = records;
        
        [dbInstance getOldestRecoreds:100 inTableType:HCTLogTableTypeMeta completion:^(NSArray<HCTLogModel *> * _Nonnull records) {
            NSArray<HCTLogModel *> *currentRecords = records;
            
            __block BOOL isEarlyData = NO;
            [latestRTentRecords enumerateObjectsUsingBlock:^(HCTLogModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                if ([reportIDS containsObject:obj.report_id]) {
                    isEarlyData = YES;
                }
            }];
            
            XCTAssert(!isEarlyData && currentRecords.count == 90, @"***Database「刪除最新n條數據」功能:異常");
            [exception fulfill];
        }];
        
    }];
    [self waitForExpectationsWithCommonTimeout];
}

3. 測試框架

1. Kiwi

BDD 框架裏的 Kiwi 可圈可點。使用 CocoaPods 引入 pod 'Kiwi'。看下面的例子

被測類(Planck 項目是一個基於 WebView 的 SDK,根據業務場景,發現針對 WebView 的大部分功能定製都是基於 WebView 的生命週期內發生的,因此參考 NodeJS 的中間件思想,設計了基於生命週期的 WebView 中間件)

#import <Foundation/Foundation.h>

@interface TPKTrustListHelper : NSObject

+(void)fetchRemoteTrustList;

+(BOOL)isHostInTrustlist:(NSString *)scheme;

+(NSArray *)trustList;

@end

測試類

SPEC_BEGIN(TPKTrustListHelperTest)
describe(@"Middleware Wrapper", ^{
    
    context(@"when get trustlist", ^{
        it(@"should get a array of string",^{
            NSArray *array = [TPKTrustListHelper trustList];
            [[array shouldNot] beNil];
            NSString *first = [array firstObject];
            [[first shouldNot] beNil];
            [[NSStringFromClass([first class]) should] equal:@"__NSCFString"];
        });
    });
    
    context(@"when check a string wether contained in trustlist ", ^{
        it(@"first string should contained in trustlist",^{
            NSArray *array = [TPKTrustListHelper trustList];
            NSString *first = [array firstObject];
            [[theValue([TPKTrustListHelper isHostInTrustlist:first]) should] equal:@(YES)];
        });
    });
});
SPEC_END

例子包含 Kiwi 的最基礎元素。SPEC_BEGINSPEC_END 表示測試類;describe 描述須要被測試的類;context 表示一個測試場景,也就是 Given->When->Then 裏的 Givenit 表示要測試的內容,也就是也就是 Given->When->Then 裏的 WhenThen。1個 describe 下能夠包含多個 context,1個 context 下能夠包含多個 it

Kiwi 的使用分爲:SpecsExpectationsMocks and StubsAsynchronous Testing 四部分。點擊能夠訪問詳細的說明文檔。

it 裏面的代碼塊是真正的測試代碼,使用鏈式調用的方式,簡單上手。

測試領域中 Mock 和 Stub 很是重要。Mock 模擬對象能夠下降對象之間的依賴,模擬出一個純淨的測試環境(相似初中物理課上「控制變量法」的思想)。Kiwi 也支持的很是好,能夠模擬對象、模擬空對象、模擬遵循協議的對象等等,點擊 Mocks and Stubs 查看。Stub 存根能夠控制某個方法的返回值,這對於方法內調用別的對象的方法返回值頗有幫助。減小對於外部的依賴,單一測試當前行爲是否符合預期。

針對異步測試,XCTest 則須要建立一個 XCTestExpectation 對象,在異步實現裏面調用該對象的 fulfill 方法,最後設置最大等待時間和完成的回調 - (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)handler; 以下例子

XCTestExpectation *exception = [self expectationWithDescription:@"測試數據庫插入功能"];
    [dbInstance removeAllLogsInTableType:HCTLogTableTypeMeta];
    NSMutableArray *insertModels = [NSMutableArray array];
    for (NSInteger index = 1; index <= 10000; index++) {
        HCTLogMetaModel *model = [[HCTLogMetaModel alloc] init];
        model.log_id = index;
      	// 。。。
        [insertModels addObject:model];
    }
    [dbInstance add:insertModels inTableType:HCTLogTableTypeMeta];
    [dbInstance recordsCountInTableType:HCTLogTableTypeMeta completion:^(NSInteger count) {
        XCTAssert(count == insertModels.count, @"**Database「數據增長」功能:異常");
        [exception fulfill];
    }];
    [self waitForExpectationsWithCommonTimeout];

2. expecta、Specta

expecta 和 Specta 都出自 orta 之手,他也是 Cocoapods 的開發者之一。太牛逼了,工程化、質量保證領域的大佬。

Specta 是一個輕量級的 BDD 測試框架,採用 DSL 模式,讓測試更接近於天然語言,所以更易讀。

特色:

  • 易於集成到項目中。在 Xcode 中勾選 Include Unit Tests ,和 XCTest 搭配使用
  • 語法很規範,對比 Kiwi 和 Specta 的文檔,發現不少東西都是相同的,也就是很規範,因此學習成本低、後期遷移到其餘框架很平滑。

Expecta 是一個匹配(斷言)框架,相比 Xcode 的斷言 XCAssert,Excepta 提供更加豐富的斷言。

特色:

  • Eepecta 沒有數據類型限制,好比 1,並不關心是 NSInteger 仍是 CGFloat
  • 鏈式編程,寫起來很舒服
  • 反向匹配,很靈活。斷言匹配用 except(...).to.equal(...),斷言不匹配則使用 .notTo 或者 .toNot
  • 延時匹配,能夠在鏈式表達式後加入 .will.willNot.after(interval)

4. 小結

Xcode 自帶的 XCTestCase 比較適合 TDD,不影響源代碼,系統獨立且不影響 App 包大小。適合簡單場景下的測試。且每一個函數在最左側又個測試按鈕,點擊後能夠單獨測試某個函數。

Kiwi 是一個強大的 BDD 框架,適合稍微複雜寫的項目,寫法舒服、功能強大,模擬對象、存根語法、異步測試等知足幾乎全部的測試場景。不能和 XCTest 繼承。

Specta 也是一個 BDD 框架,基於 XCTest 開發,能夠和 XCTest 模版集合使用。相比 Kiwi,Specta 輕量一些。開發中通常搭配 Excepta 使用。若是須要使用 Mock 和 Stud 能夠搭配 OCMock。

Excepta 是一個匹配框架,比 XCTest 的斷言則更加全面一些。

沒辦法說哪一個最好、最合理,根據項目需求選擇合適的組合。

5、網絡測試

咱們在測試某個方法的時候可能會遇到方法內部調用了網絡通訊能力,網絡請求成功,可能刷新 UI 或者給出一些成功的提示;網絡失敗或者網絡不可用則給出一些失敗的提示。因此須要對網絡通訊去看進行模擬。

iOS 中不少網絡都是基於 NSURL 系統下的類實現的。因此咱們能夠利用 NSURLProtocol 的能力來監控網絡並 mock 網絡數據。若是感興趣能夠查看這篇文章

開源項目 OHHTTPStubs 就是一個對網絡模擬的庫。它能夠攔截 HTTP 請求,返回 json 數據,定製各類頭信息。

Stub your network requests easily! Test your apps with fake network data and custom response time, response code and headers!

幾個主要類及其功能:HTTPStubsProtocol 攔截網絡請求;HTTPStubs 單例管理 HTTPStubsDescriptor 實例對象;HTTPStubsResponse 僞造 HTTP 請求。

HTTPStubsProtocol 繼承自 NSURLProtocol,能夠在 HTTP 請求發送以前對 request 進行過濾處理

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
   BOOL found = ([HTTPStubs.sharedInstance firstStubPassingTestForRequest:request] != nil);
   if (!found && HTTPStubs.sharedInstance.onStubMissingBlock) {
      HTTPStubs.sharedInstance.onStubMissingBlock(request);
   }
   return found;
}

firstStubPassingTestForRequest 方法內部會判斷請求是否須要被當前對象處理

緊接着開始發送網絡請求。實際上在 - (void)startLoading 方法中能夠用任何網絡能力去完成請求,好比 NSURLSession、NSURLConnection、AFNetworking 或其餘網絡框架。OHHTTPStubs 的作法是獲取 request、client 對象。若是 HTTPStubs 單例中包含 onStubActivationBlock 對象,則執行該 block,而後利用 responseBlock 對象返回一個 HTTPStubsResponse 響應對象。

OHHTTPStubs 的具體 API 能夠查看文檔

舉個例子,利用 Kiwi、OHHTTPStubs 測試離線包功能。代碼以下

@interface HORouterManager (Unittest)

- (void)fetchOfflineInfoIfNeeded;

@end

SPEC_BEGIN(HORouterTests)

describe(@"routerTests", ^{
    context(@"criticalPath", ^{
        __block HORouterManager *routerManager = nil;
        beforeAll(^{
            routerManager = [[HORouterManager alloc] init];
        });
        it(@"getLocalPath", ^{
            __block NSString *pagePath = nil;
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                pagePath = [routerManager filePathOfUrl:@"http://***/resource1"];
            });
            [[expectFutureValue(pagePath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil];
            
            __block NSString *rescPath = nil;
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                rescPath = [routerManager filePathOfUrl:@"http://***/resource1"];
            });
            [[expectFutureValue(rescPath) shouldEventuallyBeforeTimingOutAfter(5)] beNonNil];
        });
        it(@"fetchOffline", ^{
            [HOOfflineManager sharedInstance].offlineInfoInterval = 0;
            [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
                return [request.URL.absoluteString containsString:@"h5-offline-pkg"];
            } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) {
                NSMutableDictionary *dict = [NSMutableDictionary dictionary];
                dict[@"code"] = @(0);
                dict[@"data"] = @"f722fc3efce547897819e9449d2ac562cee9075adda79ed74829f9d948e2f6d542a92e969e39dfbbd70aa2a7240d6fa3e51156c067e8685402727b6c13328092ecc0cbc773d95f9e0603b551e9447211b0e3e72648603e3d18e529b128470fa86aeb45d16af967d1a21b3e04361cfc767b7811aec6f19c274d388ddae4c8c68e857c14122a44c92a455051ae001fa7f2b177704bdebf8a2e3277faf0053460e0ecf178549e034a086470fa3bf287abbdd0f79867741293860b8a29590d2c2bb72b749402fb53dfcac95a7744ad21fe7b9e188881d1c24047d58c9fa46b3ebf4bc42a1defc50748758b5624c6c439c182fe21d4190920197628210160cf279187444bd1cb8707362cc4c3ab7486051af088d7851846bea21b64d4a5c73bd69aafc4bb34eb0862d1525c4f9a62ce64308289e2ecbc19ea105aa2bf99af6dd5a3ff653bbe7893adbec37b44a088b0b74b80532c720c79b7bb59fda3daf85b34ef35";
                NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
                return [OHHTTPStubsResponse responseWithData:data
                                                  statusCode:200
                                                     headers:@{@"Content-Type":@"application/json"}];
            }];
            [routerManager fetchOfflineInfoIfNeeded];
            [[HOOfflineInfo shouldEventually] receive:@selector(saveToLocal:)];
        });
    });
});

SPEC_END

😂 插一嘴,我貼的代碼已經好幾回能夠看到不一樣的測試框架組合了,因此不是說選了框架 A 就完事,根據場景選擇最優解。

6、UI 測試

上面文章大篇幅的講了單元測試相關的話題,單元測試十分適合代碼質量、邏輯、網絡等內容的測試,可是針對最終產物 App 來講單元測試就不太適合了,若是測試 UI 界面的正確性、功能是否正確顯然就不太適合了。Apple 在 Xcode 7 開始推出的 UI Testing 就是蘋果本身的 UI 測試框架。

不少 UI 自動化測試框架的底層實現都依賴於 Accessibility,也就是 App 可用性。UI Accessibility 是 iOS 3.0 引入的一我的性化功能,幫助身體不便的人士方便使用 App。

Accessibility 經過對 UI 元素進行分類和標記。分類成相似按鈕、文本框、文本等類型,使用 identifier 來區分不一樣 UI 元素。無痕埋點的設計與實現裏面也使用 accessibilityIdentifier 來綁定業務數據。

  1. 使用 Xcode 自帶的 UI測試則在建立工程的時候須要勾選 「Include UI Tests」。
  2. 像單元測試意義,UI 測試方法命名以 test 開頭。將鼠標光標移到方法內,點擊 Xcode 左下方的紅色按鈕,開始錄製 UI 腳本。

UI 腳本錄製

解釋說明:

/*! Proxy for an application that may or may not be running. */
@interface XCUIApplication : XCUIElement
// ...
@end
  • XCUIApplication launch 來啓動測試。XCUIApplication 是 UIApplication 在測試進程中的代理,用來和 App 進行一些交互。

  • 使用 staticTexts來獲取當前屏幕上的靜態文本(UILabel)元素的代理。等價於 [app descendantsMatchingType:XCUIElementTypeStaticText]。XCUIElementTypeStaticText 參數是枚舉類型。

    typedef NS_ENUM(NSUInteger, XCUIElementType) {
        XCUIElementTypeAny = 0,
        XCUIElementTypeOther = 1,
        XCUIElementTypeApplication = 2,
        XCUIElementTypeGroup = 3,
        XCUIElementTypeWindow = 4,
        XCUIElementTypeSheet = 5,
        XCUIElementTypeDrawer = 6,
        XCUIElementTypeAlert = 7,
        XCUIElementTypeDialog = 8,
        XCUIElementTypeButton = 9,
        XCUIElementTypeRadioButton = 10,
        XCUIElementTypeRadioGroup = 11,
        XCUIElementTypeCheckBox = 12,
        XCUIElementTypeDisclosureTriangle = 13,
        XCUIElementTypePopUpButton = 14,
        XCUIElementTypeComboBox = 15,
        XCUIElementTypeMenuButton = 16,
        XCUIElementTypeToolbarButton = 17,
        XCUIElementTypePopover = 18,
        XCUIElementTypeKeyboard = 19,
        XCUIElementTypeKey = 20,
        XCUIElementTypeNavigationBar = 21,
        XCUIElementTypeTabBar = 22,
        XCUIElementTypeTabGroup = 23,
        XCUIElementTypeToolbar = 24,
        XCUIElementTypeStatusBar = 25,
        XCUIElementTypeTable = 26,
        XCUIElementTypeTableRow = 27,
        XCUIElementTypeTableColumn = 28,
        XCUIElementTypeOutline = 29,
        XCUIElementTypeOutlineRow = 30,
        XCUIElementTypeBrowser = 31,
        XCUIElementTypeCollectionView = 32,
        XCUIElementTypeSlider = 33,
        XCUIElementTypePageIndicator = 34,
        XCUIElementTypeProgressIndicator = 35,
        XCUIElementTypeActivityIndicator = 36,
        XCUIElementTypeSegmentedControl = 37,
        XCUIElementTypePicker = 38,
        XCUIElementTypePickerWheel = 39,
        XCUIElementTypeSwitch = 40,
        XCUIElementTypeToggle = 41,
        XCUIElementTypeLink = 42,
        XCUIElementTypeImage = 43,
        XCUIElementTypeIcon = 44,
        XCUIElementTypeSearchField = 45,
        XCUIElementTypeScrollView = 46,
        XCUIElementTypeScrollBar = 47,
        XCUIElementTypeStaticText = 48,
        XCUIElementTypeTextField = 49,
        XCUIElementTypeSecureTextField = 50,
        XCUIElementTypeDatePicker = 51,
        XCUIElementTypeTextView = 52,
        XCUIElementTypeMenu = 53,
        XCUIElementTypeMenuItem = 54,
        XCUIElementTypeMenuBar = 55,
        XCUIElementTypeMenuBarItem = 56,
        XCUIElementTypeMap = 57,
        XCUIElementTypeWebView = 58,
        XCUIElementTypeIncrementArrow = 59,
        XCUIElementTypeDecrementArrow = 60,
        XCUIElementTypeTimeline = 61,
        XCUIElementTypeRatingIndicator = 62,
        XCUIElementTypeValueIndicator = 63,
        XCUIElementTypeSplitGroup = 64,
        XCUIElementTypeSplitter = 65,
        XCUIElementTypeRelevanceIndicator = 66,
        XCUIElementTypeColorWell = 67,
        XCUIElementTypeHelpTag = 68,
        XCUIElementTypeMatte = 69,
        XCUIElementTypeDockItem = 70,
        XCUIElementTypeRuler = 71,
        XCUIElementTypeRulerMarker = 72,
        XCUIElementTypeGrid = 73,
        XCUIElementTypeLevelIndicator = 74,
        XCUIElementTypeCell = 75,
        XCUIElementTypeLayoutArea = 76,
        XCUIElementTypeLayoutItem = 77,
        XCUIElementTypeHandle = 78,
        XCUIElementTypeStepper = 79,
        XCUIElementTypeTab = 80,
        XCUIElementTypeTouchBar = 81,
        XCUIElementTypeStatusItem = 82,
    };
  • 經過 XCUIApplication 實例化對象調用 descendantsMatchingType: 方法獲得的是 XCUIElementQuery 類型。好比 @property (readonly, copy*) XCUIElementQuery *staticTexts;

    /*! Returns a query for all descendants of the element matching the specified type. */
    - (XCUIElementQuery *)descendantsMatchingType:(XCUIElementType)type;
  • descendantsMatchingType 返回全部後代的類型匹配對象。childrenMatchingType 返回當前層級子元素的類型匹配對象

    /*! Returns a query for direct children of the element matching the specified type. */
    - (XCUIElementQuery *)childrenMatchingType:(XCUIElementType)type;
  • 拿到 XCUIElementQuery 後不能直接拿到 XCUIElement。和 XCUIApplication 相似,XCUIElement 不能直接訪問 UI 元素,它是 UI 元素在測試框架中的代理。能夠經過 Accessibility 中的 frameidentifier 來獲取。

對比不少自動化測試框架都須要找出 UI 元素,也就是藉助於 Accessibilityidentifier。這裏的惟一標識生成對比爲 UIAutomation 添加自動化測試標籤的探索]

第三方 UI 自動化測試框架挺多的,能夠查看下典型的 appiummacaca

7、 測試經驗總結

TDD 寫好測試再寫業務代碼,BDD 先寫實現代碼,再寫基於行爲的測試代碼。另外一種思路是不必針對每一個類的私有方法或者每一個方法進行測試,由於等所有功能作完後針對每一個類的接口測試,通常會覆蓋據大多數的方法。等測試完看若是方法未被覆蓋,則針對性的補充 Unit Test

目前,UI 測試(appium) 仍是建議在覈心邏輯且長時間沒有改動的狀況下去作,這樣子每次發版本的時候能夠看成核心邏輯迴歸了,目前來看價值是方便後續的迭代和維護上有一些便利性。其餘的功能性測試仍是走 BDD。

對於類、函數、方法的走 TDD,老老實實寫 UT、走 UT 覆蓋率的把控。

UITesting 仍是建議在覈心邏輯且長時間沒有改動的狀況下去作,這樣子每次發版本的時候能夠看成核心邏輯迴歸,目前來看價值是方便後續的迭代和維護上有一些便利性。例如用戶中心 SDK 升級後,當時有了UITesing,基本上免去了測試人員介入。

若是是一些活動頁和邏輯常常變更的,老老實實走測試黑盒...

我以爲一直有個誤區,就是以爲自動測試是爲了質量,其實質量都是附送的,測試先行是讓開發更快更爽的

測試佔比

WWDC 這張圖也很清楚,UI 其實須要的佔比較小,仍是要靠單測驅動。

參考資料

相關文章
相關標籤/搜索