你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 沒有單元測試,何談重構

沒有單元測試,何談重構

編輯:IOS開發基礎

最近科技公司流年不利,那邊與整個硅谷唱反調的川普逆襲上台了,這邊特斯拉被評為美國最不可靠汽車品牌,據報道是因為特斯拉為Model X增加了過於復雜的功能(高科技多也怪我咯),如前門采用電動開啟方式,中排座椅實現了電動移動,所有這些功能整合在一個平台上,導致可靠性下滑。通俗解釋下就是電動門有個小bug,電動座椅又有個小bug,一堆小bug最終導致的大bug,人命關天了,本篇就來談談軟件開發中避免小bug的技術:單元測試。

本文將介紹以下內容:

  1. iOS開發中添加單元測試的方法。

  2. 如何寫單元測試用例及用例組。

  3. 介紹單元測試的一些基礎概念。

本篇作為重構的例子(想了解重構是什麼,另參見他們總在說重構,不過是重寫 ),假設了一個視頻網站的電影點播系統,每次點擊播放就會收取費用,按電影種類不同,時段不同,則收費不同,最終計算出顧客的總消費,並計算積分。這個例子的類關系比較清晰易懂,用OC語言實現,iOS開發的童鞋看起來會比較親切,心急的童鞋可以跳過源碼部分,先看後面添加單元測試的部分准備測試工具,需要了解細節時再回頭看源碼。

系統包含一個電影類,顧客類,及點播類,類關系如下圖所示:

2025746-974734521772d289.png

電影類

//
//  Movie.h
//  RefactorDemo
//
//  Created by xishi on 16/10/29.
//  Copyright ? 2016年 xs. All rights reserved.
//
typedef NS_ENUM(NSUInteger, MovieEnum) {
    MovieEnumChildrens = 2,
    MovieEnumRegular = 0,
    MovieEnumNewRelease = 1
};
@class Movie;
@interface Movie : NSObject
@property(nonatomic, copy) NSString *title;
@property(nonatomic) int priceCode;
- (id)initWithTitle:(NSString *)title
          priceCode:(int)priceCode;
@end
//
//  Movie.m
//  RefactorDemo
//
//  Created by xishi on 16/10/29.
//  Copyright ? 2016年 xs. All rights reserved.
//
#import "Movie.h"
@implementation Movie
- (id)initWithTitle:(NSString *)title
            priceCode:(int)priceCode {
    self = [super init];
    if (self) {
        _title = title;
        _priceCode = priceCode;
    }
    return self;
}
@end

點播類:

點播類定義了點播行為,關心點播了什麼電影,及點播的時段,這些都影響最終收取的費用。

//
//  Demand.h
//  RefactorDemo
//
//  Created by xishi on 16/10/29.
//  Copyright ? 2016年 xs. All rights reserved.
//
#import typedef NS_ENUM(NSUInteger, TimePeriodEnum) {
    TimePeriodEnumWorkDaytime = 1,
    TimePeriodEnumWorkNight = 2,
    TimePeriodEnumWeekend = 3
};
@class Movie;
@interface Demand : NSObject
@property(nonatomic) Movie *movie;
@property(nonatomic, assign) int timePeriod;
- (id)initWithMovie:(Movie *)movie
         timePeriod:(TimePeriodEnum)timePeriod;
@end
//
//  Demand.m
//  RefactorDemo
//
//  Created by xishi on 16/10/29.
//  Copyright ? 2016年 xs. All rights reserved.
//
#import "Demand.h"
#import "Movie.h"
@implementation Demand
- (id)initWithMovie:(Movie *)movie
         timePeriod:(TimePeriodEnum)timePeriod {
    self = [super init];
    if (self) {
        _movie = movie;
        _timePeriod = timePeriod;
    }
    return self;
}
@end

顧客類

//
//  Customer.h
//  RefactorDemo
//
//  Created by xishi on 16/10/29.
//  Copyright ? 2016年 xs. All rights reserved.
//
#import @class Demand;
@interface Customer : NSObject
- (id)initCustomerWithName:(NSString *)name;
- (void)addDemand:(Demand *)demand;
- (NSString *)statement;
@end
//
//  Customer.m
//  RefactorDemo
//
//  Created by xishi on 16/10/29.
//  Copyright ? 2016年 xs. All rights reserved.
//
#import "Customer.h"
#import "Demand.h"
#import "Movie.h"
@interface Customer () {
    NSString *_name;
    NSMutableArray *_demands;
}
@end
@implementation Customer
- (id)initCustomerWithName:(NSString *)name {
    self = [super init];
    if (self) {
        _name = name;
    }
    return self;
}
- (void)addDemand:(Demand *)demand {
    if (!_demands) {
        _demands = [[NSMutableArray alloc] init];
    }
    [_demands addObject:demand];
}
- (NSString *)statement {
    double totalAmount = 0;
    int frequentDemandPotnts = 0;
    NSMutableString *result = [NSMutableString stringWithFormat:@"%@的點播清單\\\\n", _name];
    for (Demand *aDemand in _demands) {
        double thisAmount = 0;
        // 根據不同電影定價:
        switch (aDemand.movie.priceCode) {
            case MovieEnumRegular:
                thisAmount += 2; // 普通電影2元一次
                break;
            case MovieEnumNewRelease:
                thisAmount += 3; // 新電影3元一次
                break;
            case MovieEnumChildrens:
                thisAmount += 1.5; // 兒童電影1.5元一次
        }
        // 根據不同時段定價:
        if (aDemand.timePeriod == TimePeriodEnumWorkDaytime)
            thisAmount *= 1.0; // 工作日全價
        else
            if (aDemand.timePeriod == TimePeriodEnumWeekend) {
                thisAmount *= 0.5; // 周末半價
            }
            else
                if (aDemand.timePeriod == TimePeriodEnumWorkNight){
                    thisAmount *= 1.5; // 下班1.5倍
                }
        frequentDemandPotnts++;
        // 周末點播新片積分翻倍:
        if ((aDemand.movie.priceCode == MovieEnumNewRelease) &&
            aDemand.timePeriod == TimePeriodEnumWeekend) {
            frequentDemandPotnts++;
        }
        [result appendFormat:@"\\\\t%@\\\\t%@ 元\\\\n", aDemand.movie.title, @(thisAmount)];
        totalAmount += thisAmount;
    }
    [result appendFormat:@"費用總計 %@ 元\\\\n", @(totalAmount).stringValue];
    [result appendFormat:@"獲得積分 %@", @(frequentDemandPotnts).stringValue];
    return result;
}
@end

准備測試工具

這裡選用的是XCTest,它是Xcode8中內置的測試框架,使用起來非常簡單,分以下兩種情況為項目添加測試:

1. 新建工程時添加單元測試:

2025746-f7ff1ad749130425.png

新建時添加單元測試

2.為已有工程添加單元測試

Xcode8中添加的步驟與前幾代有所不同:

2025746-62f0ad0c01476f42.png

添加Target

2025746-ed065ed913109ceb.png

用關鍵詞test快速找到Unit Testing bundle

2025746-7f1b95bda9c2c378.png

添加好單元測試後的工程結構

添加第一個測試

第一個測試是很重要的,它決定了我們後面測試的思路和方向,這裡以需要什麼測什麼為指導原則,從結果出發,所以先來看下基本的點播需求:

工作日點播一部普通影片,收費2元,積一分。

根據以上需求描述,我們在RefactorDemoTests.m添加測試方法:

- (void)testStatement_Regular {
    Movie *matrixMovie1 = [[Movie alloc] initWithTitle:@"黑客帝國1"
                                             priceCode:MovieEnumRegular];
    Demand *aDemand1 = [[Demand alloc] initWithMovie:matrixMovie1
                                          timePeriod:TimePeriodEnumWorkDaytime];
    // 顧客租賃一部:
    Customer *aCustomer = [[Customer alloc] initCustomerWithName:@"溪石"];
    [aCustomer addDemand:aDemand1];
    XCTAssertTrue([@"溪石的點播清單\\\\n"
                   @"\\\\t黑客帝國1\\\\t2 元\\\\n"
                   @"費用總計 2 元\\\\n"
                   @"獲得積分 1"
                   isEqualToString:[aCustomer statement]],
                   @"測試點播一部普通電影");
}

這個測試用例中,顧客“溪石”點播了一部老片《黑客帝國1》,由於是工作日,因此按原價收取,並積1分,詳細細節看Cutomer類源碼中的方法statement()。

按快捷鍵?U,運行測試,發現測試報錯了:

1.png

第一次運行測試報錯了

仔細檢查發現,statment()的實現中,總價與單位沒有空一格,斟酌後覺得還是空一格比較清晰,於是修改後,再次按快捷鍵?U運行測試,測試通過:

2.png

測試通過了

在單元測試中,綠色表示測試通過,紅色表示測試失敗,已經成為業界標准,XCTest遵循了這一規則。

測試用例組

通過第一個例子,我們知道了測試用例總是以test開頭,作為約定俗成,凡是test開頭的方法,都會被XCTest框架自動運行,下面我們添加對周末點播優惠的測試:

- (void)testStatement_Weekend {
    Movie *matrixMovie2 = [[Movie alloc] initWithTitle:@"黑客帝國2-重裝上陣"
                                             priceCode:MovieEnumRegular];
    Demand *aDemand2 = [[Demand alloc] initWithMovie:matrixMovie2
                                          timePeriod:TimePeriodEnumWeekend];
    Customer *aCustomer = [[Customer alloc] initCustomerWithName:@"溪石"];
    [aCustomer addDemand:aDemand2];
    XCTAssertTrue([@"溪石的點播清單\\\\n"
                   @"\\\\t黑客帝國2-重裝上陣\\\\t1 元\\\\n"
                   @"費用總計 1 元\\\\n"
                   @"獲得積分 1"
                   isEqualToString:[aCustomer statement]],
                  @"測試點播一部普通電影,周末半價");
}

這個測試用例除了電影名稱不一樣外,只是將點播時段由工作日改為了周末,以此判斷計算規則是否正確。

這時,我們已經有兩個測試用例了,為了加快測試速度,打開Xcode左側第5項的測試導航面板,可以單獨指定一個用例運行,注意圖中標記處的圖標變化:

3.png

單獨運行一個測試用例

如此,我們可以將statement需要考慮的返回情況都寫成一個個都測試用例(這裡就不一一列舉了,童鞋們可以自行實現,有問題可以評論中提出,雖然我不一定會回答),可以確保報表算法滿足全部需求。

單元測試和功能測試的差別

功能測試的目的是保證整個軟件包能正常工作,它面向的對象是客戶,保障軟件功能符合客戶的要求的質量,當然這類工作應該交由喜愛找bug的專業測試部門去處理,他們會用與開發截然不同的工具,並且不關心實現的細節(這就是你與測試人員老是話不投機的原因)。

而單元測試關注實現的細節,它的目標對象是一個類,一個方法,是我們開發人員用來驗證代碼是否有實現異常的工具,因此寫單元測試時總是尋找那些可能未處理的邊界。

測試循環

從上面的簡單用例中,我們能明顯看到以下通用步驟:

  1. 准備測試數據。

  2. 調用目標API

  3. 驗證輸出和行為

4.png

測試循環

小結

本文通過一個電影點播系統的例子,演示了以下內容:

  1. iOS開發中添加單元測試框架XCTest。

  2. 用test方法組織單元測試用例及用例組,即可統一運行,也可單獨運行。

  3. 介紹單元測試的一些基礎概念,了解單元測試的目標,及測試循環。

這些是將來進一步的重構的基礎和前提,限於篇幅,仿造對象等單元測試技術還未提及,歡迎關注溪石,且聽下回分解。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved