天天看點

(一) 初探 iOS 單元測試----------------------------------------------------------------------------------

何為單元測試

  單元測試(Unit Testing)又稱為子產品測試,是針對程式子產品軟體設計來進行正确性檢驗的測試工作。程式單元是應用的最小可測試部件。對于面向對象程式設計,最小單元就是方法,包括基類、抽象類、或者派生類中的方法。   每個理想的測試案例獨立于其它case,測試時需隔離子產品。單元測試通常由軟體開發人員編寫,用于確定所寫的代碼比對軟體需求和遵循開發目标。它的實施方式可以是手動的,或是建構自動化的一部分。   單元測試允許程式員在未來重構代碼,且確定子產品依然工作正确。這個過程是為所有方法編寫單元測試,一旦變更導緻錯誤發生,借助于單元測試可以快速定位并修複錯誤。 可讀性強的單元測試可以使程式員友善地檢查代碼片斷是否依然正常工作。良好設計的單元測試案例覆寫程式單元分支和循環條件的所有路徑。在連續的單元測試環境,通過其固有的持續維護工作,單元測試可以延續用于準确反映當任何變更發生時可執行程式和代碼的表現。借助于上述開發實踐和單元測試的覆寫,可以總是維持準确性。

單元測試的目的

1. 保證代碼的品質

  代碼可以通過編譯器檢查文法的正确性,卻不能保證代碼邏輯是正确的,尤其包含了許多單元分支的情況下,單元測試可以保證代碼的行為和結果與我們的預期和需求一緻。在測試某段代碼的行為是否和你的期望一緻時,你需要确認,在任何情況下,這段代碼是否都和你的期望一緻,譬如參數可能為空,可能的異步操作等。

2. 保證代碼的可維護性

  保證原有單元測試正确的情況下,無論如何修改單元内部代碼,測試的結果應該是正确的,且修改後不會影響到其他的子產品。

3. 保證代碼的可擴充性

  為了保證可行的可持續的單元測試,程式單元應該是低耦合的,否則,單元測試将難以進行。

單元測試的本質

1. 是一種驗證行為

  單元測試在開發前期檢驗了代碼邏輯的正确性,開發後期,無論是修改代碼内部抑或重構,測試的結果為這一切提供了可量化的保障。

2. 是一種設計行為

  為了可進行單元測試,尤其是先寫單元測試(TDD),我們将從調用者思考,從接口上思考,我們必須把程式單元設計成接口功能劃厘清晰的,易于測試的,且與外部子產品耦合性盡可能小。

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

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

4. 是程式優良的文檔

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

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

*兩種測試思想

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

  行為驅動開發(Behavior-driven development,BDD)是一種靈活軟體開發的技術,BDD的重點是通過與利益相關者的讨論取得對預期的軟體行為的清醒認識。

它通過用自然語言書寫非程式員可讀的測試用例擴充了 測試驅動開發方法(TDD)

。這讓開發者得以把精力集中在代碼應該怎麼寫,而不是技術細節上,而且也最大程度的減少了将代碼編寫者的技術語言與商業客戶、使用者、利益相關者、項目管理者等的領域語言之間來回翻譯的代價。

在iOS單元測試架構中,kiwi是BDD的代表。

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

初探 iOS 單元測試

XCTest

  Xcode內建了對單元測試的支援,XCode4.x內建的是OCUnit,到了XCode5.x時代就更新為了XCTest,XCode7.x時代XCtest還可以進行UI測試。下面我們簡單介紹下XCTest的使用。   在xcode建立項目中,預設會建一個單元測試的target,并建立一個繼承于XCTestCase的測試用例類

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

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

** 常用斷言 **
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測試專題