寫好測試,提升應用質量

相信在國內一些中小型公司,開發者很少會去寫軟件測試相關的代碼。當然這背後有一些原因在。本文就講講 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 編寫的測試用例針對的是行爲,測試範圍更大一些,適合集成測試、系統測試階段。

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

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

編寫功能、業務代碼的時候一般會遵循 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 開發過程類似下圖:

<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 的斷言則更加全面一些。

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

五、網絡測試

我們在測試某個方法的時候可能會遇到方法內部調用了網絡通信能力,網絡請求成功,可能刷新 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 其實需要的佔比較小,還是要靠單測驅動。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章