iOS單元測試從入門到應用(長文)

引言:
因為之前工作中經歷過幾次大的項目重構和組件化,所以陸陸續續學習了一些iOS單元測試相關的一些知識,以下內容是在前人基礎上的一些總結和在工程中應用的一些心得。若有不足,望多多指正

目錄

  1. 什么是單元測試
  2. 為什么要做單元測試
  3. 如何進行單元測試
    3.1. 測試準備
    3.2. 公共方法的測試
    3.3. 私有方法的測試
    3.4. 性能測試
    3.5. 運用OCMock進行測試
    3.6. 異步測試
    3.7. UITest腳本錄制
    3.8. 引入單元測試遇到的問題
  4. 擴展閱讀
    4.1. TDD
    4.2. BDD
    4.3. Kiwi
  5. 參考

1. 什么是單元測試

  • 單元測試:單元測試又稱為模塊測試, Unit Testing,是針對【程序模塊】來進行正確性檢驗的測試工作。

  • 程序模塊:程序模塊是軟件設計的最小單位,在過程化編程中,一個單元就是單個程序、函數、過程等。對于面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法,需要方法具有良好的【可測試性】

  • 可測試性:方法的可測試性提倡一個方法專注于一件事。函數式編程不具備良好的可測試性

總結:
單元測試是針對單個程序、函數、過程進行正確性檢驗的工作

2. 為什么要做單元測試

  • 單元測試作為敏捷開發實踐的組成之一,其目的是提高軟件開發的效率,維持代碼的健康性

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

  • 注:單元測試的目標是證明軟件能夠正常運行,而不是發現bug(發現bug這一目的與開發成本是正相關的,雖然發現bug是保證軟件質量的一種手段,但是很顯然這與降低軟件開發成本這一目的背道而馳)。它是對軟件質量的一種保證,例如重構之后我們需要保證軟件產品的正常運行

總結:
提高軟件開發的效率;
維持代碼的健壯性;
證明軟件能夠正常運行(而不是發現bug)

實例

先看一個簡單的Demo,因為我們項目中后臺金額相關的接口返回都是以“分”為單位,但是實際展示或計算時需要以“元”為單位,同時為了避免 Float、Double 等類型計算帶來的誤差,所以這里有一個簡單的十進制數計算工具類來進行項目中的金額轉換和計算,以下是類的聲明

/** 運算類型 */
typedef NS_ENUM(NSUInteger, RYDecimalCalculatorCalculationType) {
    RYDecimalCalculatorCalculationTypeAdding,       /** 加法 */
    RYDecimalCalculatorCalculationTypeSubtracting,  /** 減法 */
    RYDecimalCalculatorCalculationTypeMultiplying,  /** 乘法 */
    RYDecimalCalculatorCalculationTypeDividing      /** 除法 */
};

/** 舍入運算類型 */
typedef NS_ENUM(NSUInteger, RYDecimalCalculatorRoundingModeType) {
    RYDecimalCalculatorRoundingModeTypeRoundPlain,  /** 四舍五入 */
    RYDecimalCalculatorRoundingModeTypeNSRoundDown, /** 去尾法 */
    RYDecimalCalculatorRoundingModeTypeNSRoundUp    /** 進一法 */
};

@interface RYDecimalCalculator : NSObject

+ (instancetype)shareInstance;

/**
 ”分“轉換為”元“,進一法保留兩位小數
 
 @param centsString “分”字符串
 @return “元”字符串
 
 */
- (NSString *)convertCentsIntoYuan:(NSString *)centsString;

/**
 ”元“轉換為”分“,進一法保留零位小數
 
 @param yuanString “元”字符串
 @return ”分“字符串
 
 */
- (NSString *)convertYuanIntoCents:(NSString *)yuanString;

/**
 自定義十進制計算
 
 @param numOneString 計算數字一(減法為被減數,除法為被除數)
 @param numTwoString 計算數字二(減法為減數,除法為除數)
 @param calculationType 運算類型(加、減、乘、除)
 @param scale 保留精確位(小數點后零省去)
 @param roundingMode 舍入運算類型
 @return 計算結果(異常狀態返回空字符串@"")
 */
- (NSString *)calculateNumOne:(NSString *)numOneString
                       numTwo:(NSString *)numTwoString
              calculationType:(RYDecimalCalculatorCalculationType)calculationType
                        scale:(NSUInteger)scale
                 roundingMode:(RYDecimalCalculatorRoundingModeType)roundingMode;

@end

RYDecimalCalculator類提供了“元”轉“分” 和 “分”轉“元”的快捷方法,以及一個自定義的十進制數運算。通常情況下對于這樣一個工具類我們會怎么測試:

  1. 在一個便于測試的類中導入RYDecimalCalculator類
  2. mock數據,調用RYDecimalCalculator中的計算方法
  3. 運行程序,經過若干次點擊,通過斷點或者控制臺查看計算結果
  4. 繼續mock具有代表性的數據,重復2、3步
  5. 程序出錯,重復2、3、4步
    ....

對于RYDecimalCalculator類,我們使用單元測試應該如何測試,以下是在XCTest框架下編寫的用例:


@interface RYDecimalCalculatorTests : XCTestCase

@property (nonatomic, strong) RYDecimalCalculator *decimalCalculator;

@end

@implementation RYDecimalCalculatorTests

- (void)setUp {
    [super setUp];
    self.decimalCalculator = [RYDecimalCalculator shareInstance];
}

- (void)tearDown {
    [super tearDown];
}

- (void)testConvertCentsIntoYuan {
    XCTAssertEqualObjects([self.decimalCalculator convertCentsIntoYuan:@"756"], @"7.56");
}

- (void)testConvertYuanIntoCents {
    XCTAssertEqualObjects([self.decimalCalculator convertYuanIntoCents:@"235.35"], @"23535");
}

- (void)testDecimalCalculateAddingCase {
    
    NSString *numOne = @"1";
    NSString *numTow = @"0.35";
    RYDecimalCalculatorCalculationType calculationType = RYDecimalCalculatorCalculationTypeAdding;
    NSUInteger scale = 1;
    RYDecimalCalculatorRoundingModeType roundingModeType = RYDecimalCalculatorRoundingModeTypeRoundPlain;
    
    NSString *resultNumString = [self.decimalCalculator calculateNumOne:numOne numTwo:numTow calculationType:calculationType scale:scale roundingMode:roundingModeType];
    
    XCTAssertEqualObjects(resultNumString, @"1.4");
}

- (void)testDecimalCalculateSubtractingCase {
    
    NSString *numOne = @"1";
    NSString *numTow = @"0.35";
    RYDecimalCalculatorCalculationType calculationType = RYDecimalCalculatorCalculationTypeSubtracting;
    NSUInteger scale = 0;
    RYDecimalCalculatorRoundingModeType roundingModeType = RYDecimalCalculatorRoundingModeTypeNSRoundUp;
    
    NSString *resultNumString = [self.decimalCalculator calculateNumOne:numOne numTwo:numTow calculationType:calculationType scale:scale roundingMode:roundingModeType];
    
    XCTAssertEqualObjects(resultNumString, @"1");
}

- (void)testDecimalCalculateMultiplyingCase {
    
    NSString *numOne = @"10";
    NSString *numTow = @"0.35";
    RYDecimalCalculatorCalculationType calculationType = RYDecimalCalculatorCalculationTypeMultiplying;
    NSUInteger scale = 0;
    RYDecimalCalculatorRoundingModeType roundingModeType = RYDecimalCalculatorRoundingModeTypeNSRoundDown;
    
    NSString *resultNumString = [self.decimalCalculator calculateNumOne:numOne numTwo:numTow calculationType:calculationType scale:scale roundingMode:roundingModeType];
    
    XCTAssertEqualObjects(resultNumString, @"3");
}

- (void)testDecimalCalculateDividingCase {
    
    NSString *numOne = @"10";
    NSString *numTow = @"3";
    RYDecimalCalculatorCalculationType calculationType = RYDecimalCalculatorCalculationTypeDividing;
    NSUInteger scale = 2;
    RYDecimalCalculatorRoundingModeType roundingModeType = RYDecimalCalculatorRoundingModeTypeNSRoundDown;
    
    NSString *resultNumString = [self.decimalCalculator calculateNumOne:numOne numTwo:numTow calculationType:calculationType scale:scale roundingMode:roundingModeType];
    
    XCTAssertEqualObjects(resultNumString, @"3.33");
}

- (void)testDecimalCalculateUnusualCase {
    
    NSString *numOne = @"10";
    NSString *numTow = @"0";
    RYDecimalCalculatorCalculationType calculationType = RYDecimalCalculatorCalculationTypeDividing;
    NSUInteger scale = 0;
    RYDecimalCalculatorRoundingModeType roundingModeType = RYDecimalCalculatorRoundingModeTypeNSRoundDown;
    
    NSString *resultNumString = [self.decimalCalculator calculateNumOne:numOne numTwo:numTow calculationType:calculationType scale:scale roundingMode:roundingModeType];
    
    XCTAssertEqualObjects(resultNumString, @"");
}

以上測試用例編寫完成后,只需要 Command + U。如果測試通過,這個類就算單元測試通過,可以直接使用,跳過冗長的工程運行及手動點擊

可能有人會說測試用例代碼這么多有這個功夫我都測完了。但是實際上以上的測試用例大多是CV的,只是簡單的修改了輸入參數,因為XCTest提倡我們一條用例盡量只模擬一種情況。熟練后測試用例可以在很短時間完成

3. 如何進行單元測試

3.1. 測試準備

選擇測試框架
  • 方案一:XCTest + OCMock (mock對象、樁程序) + OCHamcrest(斷言擴展,非必要)

  • 方案二:Kiwi(Kiwi 與 OCMock相互不兼容)

我先后在兩次大的項目重構中采用了兩套不同的單元測試方案進行測試,因為篇幅關系,本文主要講解方案一的使用

主要測試對象
  • 網絡請求或數據重構方法
  • 工具類公共方法
  • 部分可測試視圖邏輯
  • 私有方法(若有測試必要可創建類拓展進行測試)
XCTest

XCTest是Xcode集成的一套單元測試框架,以下是它的一些基本使用

  • 基本方法
- (void)setUp {
    [super setUp];
    // 每個test方法執行前調用,在這個測試用例里進行一些通用的初始化工作
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown {
    [super tearDown];
    // 每個test方法執行后調用
    // Put teardown code here. This method is called after the invocation of each test method in the class.
}

- (void)testExample {
    // 測試方法樣例
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}

- (void)testPerformanceExample {
    //這個方法主要是做性能測試的,所謂性能測試,主要就是評估一段代碼的運行時間。該方法就是性能測試方法的樣例。
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}
  • 斷言

單元測試是以代碼測試代碼。不是靠 NSLog 來測試,而是使用斷言來測試的,提前預判條件必須滿足。

// XCTAssert(expression, ...)
// XCTAssert(條件, 不滿足條件的描述)

- (void)testExample {
  NSLog(@"自定義測試 testExample");
  int a = 3;
  XCTAssertTrue(a == 0, "a 不能等于 0");
}

無條件報錯

XCTFail. 生成一個無條件報錯XCTFail(format...)

等價測試

XCTAssertEqualObjects. 當expression1不等于expression2時報錯(或者一個對象為空,另一個不為空)XCTAssertEqualObjects(expression1, expression2, format...)

XCTAssertNotEqualObjects. 當expression1等于expression2時報錯。XCTAssertNotEqualObjects(expression1, expression2, format...)

XCTAssertEqual. 當expression1不等于expression2時報錯,這個測試用于C語言的標量。XCTAssertEqual(expression1, expression2, format...)

XCTAssertNotEqual. 當expression1等于expression2時報錯,這個測試用于C語言的標量。XCTAssertNotEqual(expression1, expression2, format...)

XCTAssertEqualWithAccuracy. 當expression1和expression2之間的差別高于accuracy 將報錯。這種測試適用于floats和doubles這些標量,兩者之間的細微差異導致它們不完全相等,但是對所有的標量都有效。XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)

XCTAssertNotEqualWithAccuracy. 當expression1和expression2之間的差別低于accuracy將產生失敗。這種測試適用于floats和doubles這些標量,兩者之間的細微差異導致它們不完全相等,但是對所有的標量都有效。XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)

空測試

XCTAssertNil. 當expression參數非nil時報錯。XCTAssertNil(expression, format...)

XCTAssertNotNil. 當expression參數為nil時報錯。XCTAssertNotNil(expression, format...)

XCTAssertTrue. 當expression計算結果為false時報錯。XCTAssertTrue(expression, format...)

XCTAssert. 當expression計算結果為false時報錯,與XCTAssertTrue同義。XCTAssert(expression, format...)

XCTAssertFalse. 當expression計算結果為true報錯。XCTAssertFalse(expression, format...)

異常測試

XCTAssertThrows.當expression不拋出異常時報錯。XCTAssertThrows(expression, format...)

XCTAssertThrowsSpecific.當expression針對指定類不拋出異常時報錯。XCTAssertThrowsSpecific(expression, exception_class, format...)

XCTAssertThrowsSpecificNamed. 當expression針對特定類和特定名字不拋出異常時報錯。對于AppKit框架或Foundation框架非常有用,拋出帶有特定名字的NSException(NSInvalidArgumentException等等)。XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, format...)

XCTAssertNoThrow. 當expression拋出異常時報錯。XCTAssertNoThrow(expression, format...)

XCTAssertNoThrowSpecific. 當expression針對指定類拋出異常時報錯。任意其他異常都可以;也就是說它不會報錯。XCTAssertNoThrowSpecific(expression, exception_class, format...)

XCTAssertNoThrowSpecificNamed. 當expression針對特定類和特定名字拋出異常時報錯。對于AppKit框架或Foundation框架非常有用,拋出帶有特定名字的NSException(NSInvalidArgumentException等等)XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, format...)

OCMock

我們要測試的方法會引用很多外部依賴的對象,而我們沒法控制這些外部依賴的對象。為了解決這個問題,我們需要用到Stub和Mock來模擬這些外部依賴的對象,從而控制它們。單獨依靠XCTest難以完成Mock或者Stub,但是結合OCMock可以在測試代碼中實現這以下功能。

  • Stub --- 樁程序, 人為地讓一個對象對某個方法返回我們事先規定好的值
    OCMStub([mockCar getCarBrand:[OCMArg any]]).andReturn(@"XXX");

[OCMArg any]表示可以為任意參數,若改為具體參數(如:張三),表示只有參數為張三時才會觸發stup,否則不會觸發

  • Mock --- 模擬對象, 一個對象, 它是對現有類的行為一種模擬(或是對現有接口實現的模擬), 它只能響應那些你添加了期望或者 stub 的方法
    // 1.nice mock - 不會在一個沒有被stub的方法被調用時拋出異常
    Car *mockCar = OCMClassMock([Car class]);
    
    // 2.vanilla mock - 在mock的生命周期中每一個方法調用都必須是stub過的方法。當調用一個沒有stub的方法的時候會拋出一個異常
    Car *mockCar = OCMStrictClassMock([Car class]);
    
    // 3.partial mock - 當一個沒有stub過的方法被調用了,這個方法會被轉發到真實的對象上。
    Car *mockCar = OCMPartialMock([Car class]);

創建測試類

對于新創建的工程我們可以勾選 Include Unit Tests 以及 Include UI Tests(后面會在UI自動化測試中講到)

image.png

對于已經創建的工程但未引入Unit Tests的工程,可以在File -> New -> Target中創建單元測試Target


image.png

假定一個Teacher類,我們需要對其進行單元測試。在單元測試Target下新建測試類,通常以“被測試類 + Tests”來命名


image.png

3.2. 公共方法的測試

Teather.h

@interface Teather : NSObject

- (float)sumStudentsGrade:(float)gradeOne gradeTwo:(float)gradeTwo;

@end

TeatherTests.m

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

@interface TeatherTests : XCTestCase

@property (nonatomic, strong) Teather *teather;

@end

@implementation TeatherTests

- (void)setUp {
    Teather *teather = [Teather new];
    self.teather = teather;
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
}

- (void)testExample {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}

- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

// 公共方法測試
- (void)testSumStudentsGrade {
    float sumGrade = [self.teather sumStudentsGrade:80 gradeTwo:90];
    XCTAssertEqual(sumGrade, 170);
}

@end

3.3. 私有方法的測試

通常情況下,類的私有方法不需要測試,因為對一個類的公共方法進行測試其私有方法也會被執行,我們僅需要對黑盒進行輸入輸出測試而不需要了解其內部的調用

特殊情況下,我們需要測試一些不被外部直接調用的方法,如生命周期或代理調用的私有方法。如果有測試必要也可以進行測試。具體方法如下

  • 對于私有方法的測試,我們可以將私有方法暴露至被測試類的.h文件中,這樣測試類就能很輕易的獲取到被測試類的私有方法。但是為了單元測試而破壞類的封裝,這樣的方式得不償失,無法滿足單元測試不影響程序代碼的要求

  • 一個可行的方式是新建一個被測試類的專用于單元測試的分類,將分類暴露給測試類來獲取其私有方法

實例

Teacher.h

@interface Teather : NSObject

- (float)sumStudentsGrade:(float)gradeOne gradeTwo:(float)gradeTwo;

@end

Teacher.m


@implementation Teather

- (float)sumStudentsGrade:(float)gradeOne gradeTwo:(float)gradeTwo {
    return gradeOne + gradeTwo;
}

- (BOOL)privateMethodAndRetuenTure {
    return YES;
}

@end

這里我們將對私有方法privateMethodAndRetuenTure進行測試

Teather+Test.h

@interface Teather (Test)

- (BOOL)privateMethodAndRetuenTure;

@end

Teather+Test.m

@implementation Teather (Test)

@end

TeatherTests.m (省去了本例無關代碼)

#import "Teather+Test.h"

@implementation TeatherTests

// 私有方法測試
- (void)testPrivateMethodAndRetuenTure {
    
    BOOL testBool = [self.teather privateMethodAndRetuenTure];
    XCTAssertEqual(testBool, YES);
}

分類中聲明了與私有方法同名的方法,我們知道方法會優先調用分類中的方法。但是分類的.m中并未作方法的實現,那會出現什么情況?IMP會轉而到Teacher.m中尋找方法的實現。這樣我們就達到了間接調用被測試類中的私有方法的目的。

因為Teather+Test.m中未做任何方法的實現,我們完全可以刪除掉這個沒有作用的.m文件。并且吧Teather+Test.h移動到測試類所在的測試目錄,保證我們主工程Target的簡潔,如下圖


image.png

3.4. 性能測試

性能測試我們只需要將被測試代碼放入measureBlock代碼塊中

// 性能測試
- (void)testPerformanceSumStudentsGrade {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
        
        float sumGrade = [self.teather sumStudentsGrade:80 gradeTwo:90];
        XCTAssertEqual(sumGrade, 170);
    }];
}

運行后會報灰色警告,因為我們沒有設置基準運行時間

image.png

我們可以直接將本次運行時間設置為基準時間

再次運行,會自動根據前面幾次運行的平均時間計算本次運行效率

image.png

3.5. 運用OCMock進行測試

對于一些方法的測試,我們可能需要依賴外部參數,這些參數可能不太容易模擬,比如

@interface Teather : NSObject

- (float)autoSumStudentsGrade:(Student *)studentOne gradeTwo:(Student *)studentTwo;

@end

@implementation Teather

- (float)autoSumStudentsGrade:(Student *)studentOne gradeTwo:(Student *)studentTwo {
    return studentOne.grade + studentTwo.grade;
}

@end

對于這個方法的測試,依賴Student對象,并且在方法的實現中調用了Student中屬性的get方法。這個時候我們就需要用到OCMock

OCMock的mock能力異常強大,mock對象、樁程序、mock協議、mock觀察者等等。本文僅討論OCMock的對象mock以及樁程序,其他用法讀者可以查閱OCMock官網或者關注我的后續分享


// 依賴外部對象
- (void)testAutoSumStudentsGrade {
    
    // 調用被測方法前,我們先創建準備依賴對象
    // OCMClassMock 的作用是返回指定類的模擬對象,它是對現有類的行為一種模擬(或是對現有接口實現的模擬), 它只能響應那些你添加了期望或者 stub 的方法
    // OCMock的對象模擬有三種方式:
    // 1.nice mock - 不會在一個沒有被stub的方法被調用時拋出異常
    Car *mockCar = OCMClassMock([Car class]);
    
    // 2.vanilla mock - 在mock的生命周期中每一個方法調用都必須是stub過的方法。當調用一個沒有stub的方法的時候會拋出一個異常
    Car *mockCar = OCMStrictClassMock([Car class]);
    
    // 3.partial mock - 當一個沒有stub過的方法被調用了,這個方法會被轉發到真實的對象上。
    Car *mockCar = OCMPartialMock([Car class]);

    // 樁程序:人為地讓一個對象對某個方法返回我們事先規定好的值
    OCMStub([mockCar getCarBrand:[OCMArg any]]).andReturn(@"XXX");
    // [OCMArg any]表示可以為任意參數,若改為具體參數(如:張三),表示只有參數為張三時才會觸發stup,否則不會觸發

    // ======================================================================================

    // mock Student對象,當調用grade的get方法時,我們指定返回結果
    Student *mockStudentOne = OCMClassMock([Student class]);
    OCMStub([mockStudentOne grade])._andReturn(OCMOCK_VALUE(90));
    
    Student *mockStudentTwo = OCMClassMock([Student class]);
    OCMStub([mockStudentTwo grade])._andReturn(OCMOCK_VALUE(100));
    
    // 調用被測方法
    float sumGrade = [self.teather autoSumStudentsGrade:mockStudentOne gradeTwo:mockStudentTwo];
  
    // 使用斷言判斷結果
    XCTAssertNotEqual(sumGrade, 200);
}

3.6. 異步測試

對于網絡請求或者異步回調方法的測試,我們可以用到XCTest提供的XCTestExpectation來進行異步測試

// 異步測試
- (void)testExpectation {
    
    // 參數 |description| 超時錯誤提示,異步操作時間超過了預設時間時才會在Log中打印出來。
    XCTestExpectation *expect = [self expectationWithDescription:@"timeout!"];
    
    // 這里我們用一個異步執行來模擬網絡請求回調
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        sleep(2); //延遲兩秒向下執行
        XCTAssertTrue(YES); //通過測試
        
        // 該方法用于表示這個異步測試結束了,每一個XCTestExpectation對象都需要對應一個fulfill,否則將會導致測試失敗
        [expect fulfill]; //告知異步測試結束
        
    });
    
    [self waitForExpectationsWithTimeout:10 handler:^(NSError *error) {
        // 等待10秒,若該測試未結束(未收到 fulfill方法)則測試結果為失敗
        // Do something when time out
    }];
}

3.7. UITest腳本錄制

對于UI的測試,我們可以利用TCTest的腳本錄制功能進行自動化測試

同樣的我們需要創建一個UI Tests的target,完成后我們同樣根據被測試類在UI Tests target下創建對應的測試類


image.png

完成后點擊錄制按鈕,可以進行腳本的錄制


UITest.gif

錄制過程中XCTestCase會為我們生成一系列自動化測試腳本,點擊測試按鈕即可執行腳本,過程中如果修改了UI元素則會導致測試不通過

3.8. 引入單元測試遇到的問題

因為我們目前的項目在一開始并沒有引入單元測試,所以后續引入單元測試的過程中難免遇到一些問題

問題1
編譯對應的測試Target編譯無法找到部分第三方框架

問題1原因
測試Target Build Setting中對應的Library Search Paths未添加對應的第三方框架

image.png

這是工程Target下以levels狀態展示Search Paths層級關系,我們可以發現主工程相對測試Target多一層由cocoapods管理的Search Paths。

問題1解決
我們可以選擇在測試Target的Search Paths中一個一個添加所有由cocoapods管理所有第三方框架。更簡單的方式我們可以通過cocoapods幫我們自動生成Search Paths

image.png

我們只需在podfile中新增兩個Target即可自動生成由pod管理的Search Paths

問題2
No type or protocol named 'YYModel';Unknown type name 'XXXXXXXType'。無法識別某些協議、類型、宏定義

問題2原因
主工程target存在pch文件,且在pch文件中導入了部分頭文件或宏定義

問題2解決
在測試target 的 prefix header中添加pch文件路徑

問題3
directory not found for option '-L/Users/XXXX/SDK/Lib'

問題3原因
Build Phases 中 Compile Sources中引入了AppDelegate + JPush;Link Binary With Libraries引入了手動導入的jshare-ios-1.6.0。而實際對于單元測試 target 不需要編譯以上類及框架

問題3解決
直接刪除Build Phases中引入框架

4. 擴展閱讀

4.1. TDD

以下引自王巍博客:

測試驅動開發(Test Driven Development,以下簡稱TDD)是保證代碼質量的不二法則,也是先進程序開發的共識。

測試驅動開發并不是一個很新鮮的概念了。軟件開發工程師們(當然包括你我)最開始學習程序編寫時,最喜歡干的事情就是編寫一段代碼,然后運行觀察結果是否正確。如果不對就返回代碼檢查錯誤,或者是加入斷點或者輸出跟蹤程序并找出錯誤,然后再次運行查看輸出是否與預想一致。如果輸出只是控制臺的一個簡單的數字或者字符那還好,但是如果輸出必須在點擊一系列按鈕之后才能在屏幕上顯示出來的東西呢?難道我們就只能一次一次地等待編譯部署,啟動程序然后操作UI,一直點到我們需要觀察的地方么?這種行為無疑是對美好生命和絢麗青春的巨大浪費。于是有一些已經浪費了無數時間的資深工程師們突然發現,原來我們可以在代碼中構建出一個類似的場景,然后在代碼中調用我們之前想檢查的代碼,并將運行的結果與我們的設想結果在程序中進行比較,如果一致,則說明了我們的代碼沒有問題,是按照預期工作的。

TDD是一種相對于普通思維的方式來說,比較極端的一種做法。我們一般能想到的是先編寫業務代碼,然后為其編寫測試代碼,用來驗證產品方法是不是按照設計工作。而TDD的思想正好與之相反,在TDD的世界中,我們應該首先根據需求或者接口情況編寫測試,然后再根據測試來編寫業務代碼,而這其實是違反傳統軟件開發中的先驗認知的。但是我們可以舉一個生活中類似的例子來說明TDD的必要性:有經驗的砌磚師傅總是會先拉一條垂線,然后沿著線砌磚,因為有直線的保證,因此可以做到筆直整齊;而新入行的師傅往往二話不說直接開工,然后在一階段完成后再用直尺垂線之類的工具進行測量和修補。TDD的好處不言自明,因為總是先測試,再編碼,所以至少你的所有代碼的public部分都應該含有必要的測試。另外,因為測試代碼實際是要使用產品代碼的,因此在編寫產品代碼前你將有一次深入思考和實踐如何使用這些代碼的機會,這對提高設計和可擴展性有很好的幫助,試想一下你測試都很難寫的接口,別人(或者自己)用起來得多糾結。在測試的準繩下,你可以有目的有方向地編碼;另外,因為有測試的保護,你可以放心對原有代碼進行重構,而不必擔心破壞邏輯。這些其實都指向了一個最終的目的:讓我們快樂安心高效地工作。

4.2. BDD

以下引自王巍博客:

XCTest(作者注:蘋果官方測試框架)是基于OCUnit的傳統測試框架,在書寫性和可讀性上都不太好。在測試用例太多的時候,由于各個測試方法是割裂的,想在某個很長的測試文件中找到特定的某個測試并搞明白這個測試是在做什么并不是很容易的事情。所有的測試都是由斷言完成的,而很多時候斷言的意義并不是特別的明確,對于項目交付或者新的開發人員加入時,往往要花上很大成本來進行理解或者轉換。另外,每一個測試的描述都被寫在斷言之后,夾雜在代碼之中,難以尋找。使用XCTest測試另外一個問題是難以進行mock或者stub,而這在測試中是非常重要的一部分。

行為驅動開發(BDD)正是為了解決上述問題而生的,作為第二代敏捷方法,BDD提倡的是通過將測試語句轉換為類似自然語言的描述,開發人員可以使用更符合大眾語言的習慣來書寫測試,這樣不論在項目交接/交付,或者之后自己修改時,都可以順利很多。如果說作為開發者的我們日常工作是寫代碼,那么BDD其實就是在講故事。一個典型的BDD的測試用例包活完整的三段式上下文,測試大多可以翻譯為Given..When..Then的格式,讀起來輕松愜意。BDD在其他語言中也已經有一些框架,包括最早的Java的JBehave和赫赫有名的Ruby的RSpec和Cucumber。而在objc社區中BDD框架也正在欣欣向榮地發展,得益于objc的語法本來就非常接近自然語言,再加上C語言宏的威力,我們是有可能寫出漂亮優美的測試的。在objc中,現在比較流行的BDD框架有cedar,specta和Kiwi。

  • 第三方BDD測試框架

OC: specta Kiwi ceder
Swift: Quick Sleipnir

4.3. Kiwi

Kiwi是一個iOS平臺十分好用的行為驅動開發(Behavior Driven Development,以下簡稱BDD)的測試框架,有著非常漂亮的語法,可以寫出結構性強,非常容易讀懂的測試。個人使用的感受上來說,相對于XCTest,Kiwi的語法更趨向于人類表達的自然語言,擁有更好的可讀性,但是學習成本相對較高且使用上不如XCTest靈活

  • Kiwi基本用法:
    Given..When..Then的三段式自然語言
describe(@"Team", ^{
    context(@"when newly created", ^{
        it(@"should have a name", ^{
            id team = [Team team];
            [[team.name should] equal:@"Black Hawks"];
        });
 
        it(@"should have 11 players", ^{
            id team = [Team team];
            [[[team should] have:11] players];
        });
    });
}); 
  • Kiwi框架關鍵字:
describe(aString,aBlock) - 描述需要測試的對象內容,也即我們三段式中的Given
context(aString,aBlock)  - context描述測試上下文,也就是這個測試在When來進行
it(aString,aBlock)       - 測試的本體,描述了這個測試應該滿足的條件
beforeEach(aBlock)       - 在scope內的每個it之前調用一次,對于context的配置代碼應該寫在這里
afterEach(aBlock)        - 在scope內的每個it之后調用一次,用于清理測試后的代碼
beforeAll(aBlock)        - 當前scope內部的所有的其他block運行之前調用一次
afterAll(aBlock)         - 當前scope內部的所有的其他block運行之后調用一次
specify(aBlock)          - 可以在里面直接書寫不需要描述的測試
pending(aString, aBlock) - 只打印一條log信息,不做測試。這個語句會給出一條警告,可以作為一開始集中書寫行為描述時還未實現的測試的提示。
xit(aString, aBlock)     - 和pending一樣,另一種寫法。因為在真正實現時測試時只需要將x刪掉就是it,但是pending語意更明確,因此還是推薦pending
  • Kiwi測試類的命名
    一個測試文件應該專注于測試一個類
    一個describe可以包含多個context,來描述類在不同情景下的行為
    一個context可以包含多個it的測試例
  • Kiwi進階用法
// stub --- 樁程序, 人為地讓一個對象對某個方法返回我們事先規定好的值
// 我們有一個 Person 類的實例,我們想要 stub 讓它返回一個固定的名字,可以這么寫:
Person *person = [Person somePerson];
[person stub:@selector(name) andReturn:@“Tom”];
 
// mock --- 模擬對象, 一個對象, 它是對現有類的行為一種模擬(或是對現有接口實現的模擬), 它只能響應那些你添加了期望或者 stub 的方法
//  創建模擬對象
id weatherForecasterMock = [WeatherForecaster mock];
 
//  設置模擬對象期望
[[weatherForecasterMock should] receive:@selector(resultWithTemprature:humidity:)andReturn:someResultWithArguments:theValue(23),theValue(50)];
 
//  用stup替換屬性
[weatherRecorder stub:@selector(weatherForecaster) andReturn:weatherForecasterMock];
// 注:對于 Kiwi 的 stub,需要注意的是它不是永久有效的,在每個 it block 的結尾 stub 都會被清空,超出范圍的方法調用將不會被 stub 截取到

本文僅簡單介紹Kiwi,詳細請參考王巍博客TDD的iOS開發初步以及Kiwi使用入門

5. 參考

iOS - UnitTests 單元測試 --- QianChia
行為驅動開發 --- 吳迪
TDD的iOS開發初步以及Kiwi使用入門 --- 王巍

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380