沒(méi)有單元測(cè)試,何談重構(gòu)

最近科技公司流年不利,那邊與整個(gè)硅谷唱反調(diào)的川普逆襲上臺(tái)了,這邊特斯拉被評(píng)為美國(guó)最不可靠汽車品牌,據(jù)報(bào)道是因?yàn)樘厮估瓰镸odel X增加了過(guò)于復(fù)雜的功能(高科技多也怪我咯),如前門(mén)采用電動(dòng)開(kāi)啟方式,中排座椅實(shí)現(xiàn)了電動(dòng)移動(dòng),所有這些功能整合在一個(gè)平臺(tái)上,導(dǎo)致可靠性下滑。通俗解釋下就是電動(dòng)門(mén)有個(gè)小bug,電動(dòng)座椅又有個(gè)小bug,一堆小bug最終導(dǎo)致的大bug,人命關(guān)天了,本篇就來(lái)談?wù)勡浖_(kāi)發(fā)中避免小bug的技術(shù):?jiǎn)卧獪y(cè)試。

本文將介紹以下內(nèi)容:

  1. iOS開(kāi)發(fā)中添加單元測(cè)試的方法。
  2. 如何寫(xiě)單元測(cè)試用例及用例組。
  3. 介紹單元測(cè)試的一些基礎(chǔ)概念。

本篇作為重構(gòu)的例子(想了解重構(gòu)是什么,另參見(jiàn)他們總在說(shuō)重構(gòu),不過(guò)是重寫(xiě) ),假設(shè)了一個(gè)視頻網(wǎng)站的電影點(diǎn)播系統(tǒng),每次點(diǎn)擊播放就會(huì)收取費(fèi)用,按電影種類不同,時(shí)段不同,則收費(fèi)不同,最終計(jì)算出顧客的總消費(fèi),并計(jì)算積分。這個(gè)例子的類關(guān)系比較清晰易懂,用OC語(yǔ)言實(shí)現(xiàn),iOS開(kāi)發(fā)的童鞋看起來(lái)會(huì)比較親切,心急的童鞋可以跳過(guò)源碼部分,先看后面添加單元測(cè)試的部分準(zhǔn)備測(cè)試工具,需要了解細(xì)節(jié)時(shí)再回頭看源碼。

系統(tǒng)包含一個(gè)<u>電影類</u>,<u>顧客類</u>,及<u>點(diǎn)播類</u>,類關(guān)系如下圖所示:


類關(guān)系圖.png

<u>電影類</u>

//
//  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

<u>點(diǎn)播類</u>:
點(diǎn)播類定義了點(diǎn)播行為,關(guān)心點(diǎn)播了什么電影,及點(diǎn)播的時(shí)段,這些都影響最終收取的費(fèi)用。

//
//  Demand.h
//  RefactorDemo
//
//  Created by xishi on 16/10/29.
//  Copyright ? 2016年 xs. All rights reserved.
//

#import <Foundation/Foundation.h>
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

<u>顧客類</u>

//
//  Customer.h
//  RefactorDemo
//
//  Created by xishi on 16/10/29.
//  Copyright ? 2016年 xs. All rights reserved.
//

#import <Foundation/Foundation.h>

@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:@"%@的點(diǎn)播清單\\\\n", _name];
    for (Demand *aDemand in _demands) {
        double thisAmount = 0;
        
        // 根據(jù)不同電影定價(jià):
        switch (aDemand.movie.priceCode) {
            case MovieEnumRegular:
                thisAmount += 2; // 普通電影2元一次
                break;
                
            case MovieEnumNewRelease:
                thisAmount += 3; // 新電影3元一次
                break;
                
            case MovieEnumChildrens:
                thisAmount += 1.5; // 兒童電影1.5元一次
        }
        
        // 根據(jù)不同時(shí)段定價(jià):
        if (aDemand.timePeriod == TimePeriodEnumWorkDaytime)
            thisAmount *= 1.0; // 工作日全價(jià)
        else
            if (aDemand.timePeriod == TimePeriodEnumWeekend) {
                thisAmount *= 0.5; // 周末半價(jià)
            }
            else
                if (aDemand.timePeriod == TimePeriodEnumWorkNight){
                    thisAmount *= 1.5; // 下班1.5倍
                }
        
        frequentDemandPotnts++;
        // 周末點(diǎn)播新片積分翻倍:
        if ((aDemand.movie.priceCode == MovieEnumNewRelease) &&
            aDemand.timePeriod == TimePeriodEnumWeekend) {
            frequentDemandPotnts++;
        }
        
        [result appendFormat:@"\\\\t%@\\\\t%@ 元\\\\n", aDemand.movie.title, @(thisAmount)];
        totalAmount += thisAmount;
    }
    
    [result appendFormat:@"費(fèi)用總計(jì) %@ 元\\\\n", @(totalAmount).stringValue];
    [result appendFormat:@"獲得積分 %@", @(frequentDemandPotnts).stringValue];
    
    return result;
}
@end

<p id="jump"></p>

準(zhǔn)備測(cè)試工具


這里選用的是XCTest,它是Xcode8中內(nèi)置的測(cè)試框架,使用起來(lái)非常簡(jiǎn)單,分以下兩種情況為項(xiàng)目添加測(cè)試:

1. 新建工程時(shí)添加單元測(cè)試:

新建時(shí)添加單元測(cè)試

2.為已有工程添加單元測(cè)試

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


添加Target
用關(guān)鍵詞test快速找到Unit Testing bundle

添加好單元測(cè)試后的工程結(jié)構(gòu)

添加第一個(gè)測(cè)試


第一個(gè)測(cè)試是很重要的,它決定了我們后面測(cè)試的思路和方向,這里以需要什么測(cè)什么為指導(dǎo)原則,從結(jié)果出發(fā),所以先來(lái)看下基本的點(diǎn)播需求:

工作日點(diǎn)播一部普通影片,收費(fèi)2元,積一分。

根據(jù)以上需求描述,我們?cè)?code>RefactorDemoTests.m添加測(cè)試方法:

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

這個(gè)測(cè)試用例中,顧客“溪石”點(diǎn)播了一部老片《黑客帝國(guó)1》,由于是工作日,因此按原價(jià)收取,并積1分,詳細(xì)細(xì)節(jié)看Cutomer類源碼中的方法statement()。
按快捷鍵?U,運(yùn)行測(cè)試,發(fā)現(xiàn)測(cè)試報(bào)錯(cuò)了:

第一次運(yùn)行測(cè)試報(bào)錯(cuò)了

仔細(xì)檢查發(fā)現(xiàn),statment()的實(shí)現(xiàn)中,總價(jià)與單位沒(méi)有空一格,斟酌后覺(jué)得還是空一格比較清晰,于是修改后,再次按快捷鍵?U運(yùn)行測(cè)試,測(cè)試通過(guò):

測(cè)試通過(guò)了

在單元測(cè)試中,綠色表示測(cè)試通過(guò),紅色表示測(cè)試失敗,已經(jīng)成為業(yè)界標(biāo)準(zhǔn),XCTest遵循了這一規(guī)則。

測(cè)試用例組


通過(guò)第一個(gè)例子,我們知道了測(cè)試用例總是以test開(kāi)頭,作為約定俗成,凡是test開(kāi)頭的方法,都會(huì)被XCTest框架自動(dòng)運(yùn)行,下面我們添加對(duì)周末點(diǎn)播優(yōu)惠的測(cè)試:

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

這個(gè)測(cè)試用例除了電影名稱不一樣外,只是將點(diǎn)播時(shí)段由工作日改為了周末,以此判斷計(jì)算規(guī)則是否正確。
這時(shí),我們已經(jīng)有兩個(gè)測(cè)試用例了,為了加快測(cè)試速度,打開(kāi)Xcode左側(cè)第5項(xiàng)的測(cè)試導(dǎo)航面板,可以單獨(dú)指定一個(gè)用例運(yùn)行,注意圖中標(biāo)記處的圖標(biāo)變化:

單獨(dú)運(yùn)行一個(gè)測(cè)試用例

如此,我們可以將statement需要考慮的返回情況都寫(xiě)成一個(gè)個(gè)都測(cè)試用例(這里就不一一列舉了,童鞋們可以自行實(shí)現(xiàn),有問(wèn)題可以評(píng)論中提出,雖然我不一定會(huì)回答),可以確保報(bào)表算法滿足全部需求。

單元測(cè)試和功能測(cè)試的差別


功能測(cè)試的目的是保證整個(gè)軟件包能正常工作,它面向的對(duì)象是客戶,保障軟件功能符合客戶的要求的質(zhì)量,當(dāng)然這類工作應(yīng)該交由喜愛(ài)找bug的專業(yè)測(cè)試部門(mén)去處理,他們會(huì)用與開(kāi)發(fā)截然不同的工具,并且不關(guān)心實(shí)現(xiàn)的細(xì)節(jié)(這就是你與測(cè)試人員老是話不投機(jī)的原因)。
單元測(cè)試關(guān)注實(shí)現(xiàn)的細(xì)節(jié),它的目標(biāo)對(duì)象是一個(gè)類,一個(gè)方法,是我們開(kāi)發(fā)人員用來(lái)驗(yàn)證代碼是否有實(shí)現(xiàn)異常的工具,因此寫(xiě)單元測(cè)試時(shí)總是尋找那些可能未處理的邊界。

測(cè)試循環(huán)

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

  1. 準(zhǔn)備測(cè)試數(shù)據(jù)。
  2. 調(diào)用目標(biāo)API
  3. 驗(yàn)證輸出和行為
測(cè)試循環(huán)

小結(jié)

本文通過(guò)一個(gè)電影點(diǎn)播系統(tǒng)的例子,演示了以下內(nèi)容:

  1. iOS開(kāi)發(fā)中添加單元測(cè)試框架XCTest。
  2. 用test方法組織單元測(cè)試用例及用例組,即可統(tǒng)一運(yùn)行,也可單獨(dú)運(yùn)行。
  3. 介紹單元測(cè)試的一些基礎(chǔ)概念,了解單元測(cè)試的目標(biāo),及測(cè)試循環(huán)。

這些是將來(lái)進(jìn)一步的重構(gòu)的基礎(chǔ)和前提,限于篇幅,仿造對(duì)象等單元測(cè)試技術(shù)還未提及,歡迎關(guān)注溪石,且聽(tīng)下回分解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn):http://www.lxweimin.com/p/d5fca0185e83 Xcode測(cè)試 前言 總算在今天把...
    測(cè)試小螞蟻閱讀 3,020評(píng)論 0 20
  • 本文將介紹以下內(nèi)容: iOS開(kāi)發(fā)中添加單元測(cè)試的方法。 如何寫(xiě)單元測(cè)試用例及用例組。 介紹單元測(cè)試的一些基礎(chǔ)概念。...
    星捷閱讀 419評(píng)論 0 0
  • 文章來(lái)自:http://blog.csdn.net/mj813/article/details/52451355 ...
    好大一只鵬閱讀 9,216評(píng)論 2 126
  • 1.測(cè)試與軟件模型 軟件開(kāi)發(fā)生命周期模型指的是軟件開(kāi)發(fā)全過(guò)程、活動(dòng)和任務(wù)的結(jié)構(gòu)性框架。軟件項(xiàng)目的開(kāi)發(fā)包括:需求、設(shè)...
    Mr希靈閱讀 22,002評(píng)論 7 278
  • 代碼覆蓋 代碼覆蓋率是Xcode7的一項(xiàng)功能,能夠顯示和測(cè)量你有多少的代碼執(zhí)行了測(cè)試。隨著你代碼的覆蓋你可以確定你...
    許漠顏閱讀 1,764評(píng)論 3 20