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

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

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

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

本篇作為重構(gòu)的例子(想了解重構(gòu)是什么,另參見他們總在說重構(gòu)旺入,不過是重寫 )兑凿,假設(shè)了一個(gè)視頻網(wǎng)站的電影點(diǎn)播系統(tǒng),每次點(diǎn)擊播放就會收取費(fèi)用茵瘾,按電影種類不同礼华,時(shí)段不同,則收費(fèi)不同拗秘,最終計(jì)算出顧客的總消費(fèi)卓嫂,并計(jì)算積分。這個(gè)例子的類關(guān)系比較清晰易懂聘殖,用OC語言實(shí)現(xiàn),iOS開發(fā)的童鞋看起來會比較親切行瑞,心急的童鞋可以跳過源碼部分奸腺,先看后面添加單元測試的部分準(zhǔn)備測試工具,需要了解細(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)備測試工具


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

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

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

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

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


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

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

添加第一個(gè)測試


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

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

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

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

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

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

測試通過了

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

測試用例組


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

- (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]],
                  @"測試點(diǎn)播一部普通電影,周末半價(jià)");
}

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

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

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

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


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

測試循環(huán)

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

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

小結(jié)

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末听怕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子虑绵,更是在濱河造成了極大的恐慌尿瞭,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翅睛,死亡現(xiàn)場離奇詭異声搁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)捕发,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門疏旨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人扎酷,你說我怎么就攤上這事檐涝。” “怎么了法挨?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵谁榜,是天一觀的道長。 經(jīng)常有香客問我凡纳,道長窃植,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任荐糜,我火速辦了婚禮巷怜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘狞尔。我一直安慰自己丛版,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布偏序。 她就那樣靜靜地躺著页畦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪研儒。 梳的紋絲不亂的頭發(fā)上豫缨,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機(jī)與錄音端朵,去河邊找鬼好芭。 笑死,一個(gè)胖子當(dāng)著我的面吹牛冲呢,可吹牛的內(nèi)容都是我干的舍败。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼邻薯!你這毒婦竟也來了裙戏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤厕诡,失蹤者是張志新(化名)和其女友劉穎累榜,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灵嫌,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡壹罚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寿羞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猖凛。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖稠曼,靈堂內(nèi)的尸體忽然破棺而出形病,到底是詐尸還是另有隱情,我是刑警寧澤霞幅,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布漠吻,位于F島的核電站,受9級特大地震影響司恳,放射性物質(zhì)發(fā)生泄漏途乃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一扔傅、第九天 我趴在偏房一處隱蔽的房頂上張望耍共。 院中可真熱鬧,春花似錦猎塞、人聲如沸试读。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钩骇。三九已至,卻和暖如春铝量,著一層夾襖步出監(jiān)牢的瞬間倘屹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工慢叨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纽匙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓拍谐,卻偏偏與公主長得像烛缔,于是被迫代替她去往敵國和親馏段。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

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

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