寫好測試,提升應用質量

fantasticbaby發表於2020-11-20
相信在國內一些中小型公司,開發者很少會去寫軟體測試相關的程式碼。當然這背後有一些原因在。本文就講講 iOS 開發中的軟體測試相關的內容。

一、 測試的重要性

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

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

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

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

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

二、軟體測試

1. 分類

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

合理應用軟體測試技術,就可以規避掉第一部分的3個場景下的問題。

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

軟體測試從測試範圍分為:單元測試、整合測試、系統測試、迴歸測試、驗收測試(有些公司會談到“冒煙測試“,這個詞的精確定義不知道,但是學軟體測試課的時候按照範圍就只有上述幾個分類)。工程師自己負責的是單元測試。測試工程師、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 編寫的測試用例針對的是行為,測試範圍更大一些,適合整合測試、系統測試階段。

三、 單元測試編碼規範

本文的主要重點是針對日常開發階段工程師可以做的事情,也就是單元測試而展開。

編寫功能、業務程式碼的時候一般會遵循 kiss 原則 ,所以類、方法、函式往往不會太大,分層設計越好、職責越單一、耦合度越低的程式碼越適合做單元測試,單元測試也倒逼開發過程中程式碼分層、解耦。

可能某個功能的實現程式碼有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

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

單元測試是按照測試範圍來劃分的。TDD、BDD 是按照開發模式來劃分的。因此就有各種排列組合,這裡我們只關心單元測試下的 TDD、BDD 方案。

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

1. TDD

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

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

TDD 開發過程類似下圖:

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

丟擲一個問題: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 的斷言則更加全面一些。

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

五、網路測試

我們在測試某個方法的時候可能會遇到方法內部呼叫了網路通訊能力,網路請求成功,可能重新整理 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 就完事,根據場景選擇最優解。

六、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

七、 測試經驗總結

TDD 寫好測試再寫業務程式碼,BDD 先寫實現程式碼,再寫基於行為的測試程式碼。另一種思路是沒必要針對每個類的私有方法或者每個方法進行測試,因為等全部功能做完後針對每個類的介面測試,一般會覆蓋據大多數的方法。等測試完看如果方法未被覆蓋,則針對性的補充 Unit Test

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

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

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

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

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

測試佔比

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

參考資料

相關文章