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

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

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

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

本篇作為重構(gòu)的例子(想了解重構(gòu)是什么,另參見他們總在說重構(gòu),不過是重寫 ),假設(shè)了一個(gè)視頻網(wǎng)站的電影點(diǎn)播系統(tǒng),每次點(diǎn)擊播放就會(huì)收取費(fèi)用,按電影種類不同,時(shí)段不同,則收費(fèi)不同,最終計(jì)算出顧客的總消費(fèi),并計(jì)算積分。這個(gè)例子的類關(guān)系比較清晰易懂,用OC語言實(shí)現(xiàn),iOS開發(fā)的童鞋看起來會(huì)比較親切,心急的童鞋可以跳過源碼部分,先看后面添加單元測(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è)試框架,使用起來非常簡單,分以下兩種情況為項(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ā),所以先來看下基本的點(diǎn)播需求:

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

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

- (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([@"溪石的點(diǎn)播清單\\\\n"
                   @"\\\\t黑客帝國1\\\\t2 元\\\\n"
                   @"費(fèi)用總計(jì) 2 元\\\\n"
                   @"獲得積分 1"
                   isEqualToString:[aCustomer statement]],
                   @"測(cè)試點(diǎn)播一部普通電影");
    
}

這個(gè)測(cè)試用例中,顧客“溪石”點(diǎn)播了一部老片《黑客帝國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à)與單位沒有空一格,斟酌后覺得還是空一格比較清晰,于是修改后,再次按快捷鍵?U運(yùn)行測(cè)試,測(cè)試通過:

測(cè)試通過了

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

測(cè)試用例組


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

- (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([@"溪石的點(diǎn)播清單\\\\n"
                   @"\\\\t黑客帝國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è)試速度,打開Xcode左側(cè)第5項(xiàng)的測(cè)試導(dǎo)航面板,可以單獨(dú)指定一個(gè)用例運(yùn)行,注意圖中標(biāo)記處的圖標(biāo)變化:

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

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

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


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

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

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

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

小結(jié)

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,025評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,642評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,970評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,157評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,410評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評(píng)論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,896評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,157評(píng)論 2 375

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

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