(一) 初探 iOS 單元測試

何爲單元測試

  單元測試(Unit Testing)又稱爲模塊測試,是針對程序模塊軟件設計來進行正確性檢驗的測試工做。程序單元是應用的最小可測試部件。對於面向對象編程,最小單元就是方法,包括基類、抽象類、或者派生類中的方法。   每一個理想的測試案例獨立於其它case,測試時需隔離模塊。單元測試一般由軟件開發人員編寫,用於確保所寫的代碼匹配軟件需求和遵循開發目標。它的實施方式能夠是手動的,或是構建自動化的一部分。   單元測試容許程序員在將來重構代碼,且確保模塊依然工做正確。這個過程是爲全部方法編寫單元測試,一旦變動致使錯誤發生,藉助於單元測試能夠快速定位並修復錯誤。 可讀性強的單元測試可使程序員方便地檢查代碼片段是否依然正常工做。良好設計的單元測試案例覆蓋程序單元分支和循環條件的全部路徑。在連續的單元測試環境,經過其固有的持續維護工做,單元測試能夠延續用於準確反映當任何變動發生時可執行程序和代碼的表現。藉助於上述開發實踐和單元測試的覆蓋,能夠老是維持準確性。php

單元測試的目的

1. 保證代碼的質量

  代碼能夠經過編譯器檢查語法的正確性,卻不能保證代碼邏輯是正確的,尤爲包含了許多單元分支的狀況下,單元測試能夠保證代碼的行爲和結果與咱們的預期和需求一致。在測試某段代碼的行爲是否和你的指望一致時,你須要確認,在任何狀況下,這段代碼是否都和你的指望一致,譬如參數可能爲空,可能的異步操做等。html

2. 保證代碼的可維護性

  保證原有單元測試正確的狀況下,不管如何修改單元內部代碼,測試的結果應該是正確的,且修改後不會影響到其餘的模塊。ios

3. 保證代碼的可擴展性

  爲了保證可行的可持續的單元測試,程序單元應該是低耦合的,不然,單元測試將難以進行。git

單元測試的本質

1. 是一種驗證行爲

  單元測試在開發前期檢驗了代碼邏輯的正確性,開發後期,不管是修改代碼內部抑或重構,測試的結果爲這一切提供了可量化的保障。程序員

2. 是一種設計行爲

  爲了可進行單元測試,尤爲是先寫單元測試(TDD),咱們將從調用者思考,從接口上思考,咱們必須把程序單元設計成接口功能劃分清晰的,易於測試的,且與外部模塊耦合性儘量小。github

3. 是一種快速回歸的方式

  在原代碼基礎上開發及修改功能時,單元測試是一種快捷,可靠的迴歸。express

4. 是程序優良的文檔

  從效果上而言,單元測試就像是能執行的文檔,說明了在你用各類條件調用代碼時,你所能指望這段代碼完成的功能。編程

-----------------------------------------

*兩種測試思想

  測試驅動開發(Test-driven development,TDD)是一種軟件開發過程當中的應用方法,由極限編程中倡導,以其倡導先寫測試程序,而後編碼實現其功能得名。測試驅動開發是戴兩頂帽子思考的開發方式:先戴上實現功能的帽子,在測試的輔助下,快速實現其功能;再戴上重構的帽子,在測試的保護下,經過去除冗餘的代碼,提升代碼質量。測試驅動着整個開發過程:首先,驅動代碼的設計和功能的實現;其後,驅動代碼的再設計和重構。xcode

  行爲驅動開發(Behavior-driven development,BDD)是一種敏捷軟件開發的技術,BDD的重點是經過與利益相關者的討論取得對預期的軟件行爲的清醒認識。它經過用天然語言書寫非程序員可讀的測試用例擴展了 測試驅動開發方法(TDD)。這讓開發者得以把精力集中在代碼應該怎麼寫,而不是技術細節上,並且也最大程度的減小了將代碼編寫者的技術語言與商業客戶、用戶、利益相關者、項目管理者等的領域語言之間來回翻譯的代價。bash

在iOS單元測試框架中,kiwi是BDD的表明。

-----------------------------------------

初探 iOS 單元測試

XCTest

  Xcode集成了對單元測試的支持,XCode4.x集成的是OCUnit,到了XCode5.x時代就升級爲了XCTest,XCode7.x時代XCtest還能夠進行UI測試。下面咱們簡單介紹下XCTest的使用。   在xcode新建項目中,默認會建一個單元測試的target,並創建一個繼承於XCTestCase的測試用例類

XCTest-Target
  若項目中沒有,能夠在 File->New->Target->ios-test->iOS Unit Testing Bundle 新建一個測試target。
Target-New
  本例實現了一個個稅計算方法,在測試用例中測試輸入後輸出是否符合結果。

ASUnitTestFirstDemoTests.m
#import <XCTest/XCTest.h>
#import "ASRevenueBL.h"

@interface ASUnitTestFirstDemoTests : XCTestCase

@property (nonatomic, strong) ASRevenueBL *revenueBL;

@end

@implementation ASUnitTestFirstDemoTests

- (void)setUp {
    [super setUp];
    self.revenueBL = [[ASRevenueBL alloc] init];
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown {
    self.revenueBL = nil;

    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

- (void)testLevel1 {      // 異步測試
  double revenue = 5000;
  double tax = [self.revenueBL calculate:revenue];
  XCTAssertEqual(tax, 45.0, @"用例1測試失敗");
  XCTAssertTrue(tax == 45.0);
}

- (void)testLevel2 {
  XCTestExpectation *exp = [self expectationWithDescription:@"超時"];
  NSOperationQueue *queue = [[NSOperationQueue alloc]init];
  [queue addOperationWithBlock:^{
    double revenue = 1500;
    double tax = [self.revenueBL calculate:revenue];
    sleep(1);
    XCTAssertEqual(tax, 45.0, @"用例2測試失敗");
    [exp fulfill];  // exp結束
  }];
  
  [self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
    if (error) {
      NSLog(@"Timeout Error: %@", error);
    }
  }];
}
- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
       for (int a = 0; a<10000; a+=a) {
            NSLog(@"%zd", a);
      }
    }];
}

@end

複製代碼
ASRevenueBL.m
#import "ASRevenueBL.h"

#define baseNum 3500.0

@implementation ASRevenueBL

- (double)calculate:(double)revenue {
  double tax = 0.0;
  double dbTaxRevenue = revenue - baseNum;
  if (dbTaxRevenue <= 1500) {
    tax = dbTaxRevenue * 0.03;
  } else if (dbTaxRevenue > 1500 && dbTaxRevenue <= 4500) {
    tax = dbTaxRevenue * 0.1 - 105;
  } else if (dbTaxRevenue > 4500 && dbTaxRevenue <= 9000) {
    tax = dbTaxRevenue * 0.2 - 555;
  } else if (dbTaxRevenue > 9000 && dbTaxRevenue <= 35000) {
    tax = dbTaxRevenue * 0.25 - 1005;
  } else if (dbTaxRevenue > 35000 && dbTaxRevenue <= 55000) {
    tax = dbTaxRevenue * 0.3 - 2755;
  } else if (dbTaxRevenue > 55000 && dbTaxRevenue <= 80000) {
    tax = dbTaxRevenue * 0.35 - 5505;
  } else if (dbTaxRevenue > 80000) {
    tax = dbTaxRevenue * 0.45 - 13505;
  }
  return tax;
}

@end
複製代碼
XCTest經常使用方法介紹:
- (void)setUp; // 測試開始前調用,能夠初始化一些對象和變量
 - (void)tearDown; // 測試結束後調用
 - (void)test##Name; // 含有test前綴無參數無返回的方法都爲一個測試方法
 - (void)measureBlock:((void (^)(void)))block;  // 測量執行時間
 - (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)handler; // 多少秒exception不fullfill就報錯
 - (XCTestExpectation *)expectationForNotification:(NSNotificationName)notificationName object:(nullable id)objectToObserve handler:(nullable XCNotificationExpectationHandler)handler;  // 匹配到通知fullfill
 - (XCTestExpectation *)expectationForPredicate:(NSPredicate *)predicate evaluatedWithObject:(id)object handler:(nullable XCPredicateExpectationHandler)handler;  // predicate 返回true測試fullfill
...
複製代碼
測試結果

product-test 或 command + u即啓動test

測試結果1

測試結果2

** 經常使用斷言 **
XCTAssertNil(a1, ...)爲空判斷,expression爲空時經過
XCTAssert(expression, ...)當expression值爲TRUE時經過;
XCTAssertTrue(expression, format...)當expression值爲TRUE時經過;
XCTAssertEqual(e1, e2, ...) e1 == e2經過;
XCTAssertThrows(expression, format...)當expression拋出異常時經過;
XCTAssertThrowsSpecific(expression, specificException, format...) 當expression拋出specificException異常時經過;
複製代碼

testLevel1經過revenueBL計算出來的tax與預期相同,測試經過;testLevel2經過revenueBL計算出來的tax與預期不一樣,測試不經過,反映出了程序一些邏輯漏洞;testPerformanceExample中的平均執行時間比基準值低,測試經過。

命令行

在命令行中也能夠啓動測試,便於持續集成。

Assuner$ cd Desktop/
Desktop Assuner$ cd ASUnitTestFirstDemo/
ASUnitTestFirstDemo Assuner$ xcodebuild test -project ASUnitTestFirstDemo.xcodeproj -scheme ASUnitTestFirstDemo -destination 'platform=iOS Simulator,OS=11.0,name=iPhone 7' // 能夠有多個destination
複製代碼

結果

Test Suite 'All tests' started at 2017-09-11 11:12:16.348
Test Suite 'ASUnitTestFirstDemoTests.xctest' started at 2017-09-11 11:12:16.349
Test Suite 'ASUnitTestFirstDemoTests' started at 2017-09-11 11:12:16.349
Test Case '-[ASUnitTestFirstDemoTests testLevel1]' started.
Test Case '-[ASUnitTestFirstDemoTests testLevel1]' passed (0.001 seconds).
Test Case '-[ASUnitTestFirstDemoTests testLevel2]' started.
/Users/liyongguang-eleme-iOS-Development/Desktop/ASUnitTestFirstDemo/ASUnitTestFirstDemoTests/ASUnitTestFirstDemoTests.m:46: error: -[ASUnitTestFirstDemoTests testLevel2] : ((tax) equal to (45.0)) failed: ("-60") is not equal to ("45") - 用例2測試失敗
Test Case '-[ASUnitTestFirstDemoTests testLevel2]' failed (1.007 seconds).
Test Suite 'ASUnitTestFirstDemoTests' failed at 2017-09-11 11:12:17.358.
	 Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.009) seconds
Test Suite 'ASUnitTestFirstDemoTests.xctest' failed at 2017-09-11 11:12:17.359.
	 Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.010) seconds
Test Suite 'All tests' failed at 2017-09-11 11:12:17.360.
	 Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.012) seconds
Failing tests:
	-[ASUnitTestFirstDemoTests testLevel2]
** TEST FAILED **

複製代碼

若是是workspace

xcodebuild -workspace ASKiwiTest.xcworkspace -scheme ASKiwiTest-Example -destination 'platform=iOS Simulator,OS=11.0,name=iPhone 7' test
複製代碼

每一個test方法都會跑一遍,並給出結果描述。

謝謝觀看!若有錯誤請多指正

參考閱讀

維基百科 man xcodebuild XCTestCase cocoaChina測試專題

相關文章
相關標籤/搜索