iOS開(kāi)發(fā)——TDD、BDD方法以及Kiwi單元測(cè)試框架

TDD和BDD

在GitBook上看過(guò)一篇文章亭病,一個(gè)不寫(xiě)單元測(cè)試的程序員不是一個(gè)好的攻城獅鹅很。坦白的說(shuō),在Objective-C這個(gè)領(lǐng)域的里罪帖,我見(jiàn)過(guò)的會(huì)主動(dòng)寫(xiě)單元測(cè)試的程序員還是比較少的促煮。當(dāng)然了,在那些大的開(kāi)源項(xiàng)目里整袁,我還是見(jiàn)到過(guò)很多單元測(cè)試的應(yīng)用菠齿。

于是也就促使我想總結(jié)總結(jié)自己現(xiàn)在對(duì)單元測(cè)試的理解。眾所周知蘋(píng)果在Xcode5中引入了XCTest框架替換了原來(lái)的SenTestingKit坐昙。這也顯示了蘋(píng)果一直致力于在iOS開(kāi)發(fā)中集成更方便可用的測(cè)試绳匀。但是我一直覺(jué)得XCTest的斷言可讀性較差,如果是讓他人來(lái)閱讀這段單元測(cè)試炸客,會(huì)比較的花費(fèi)精力疾棵。

再進(jìn)入討論單元測(cè)試之前,我們來(lái)談?wù)劜灰粯訙y(cè)試思想

  • 行為驅(qū)動(dòng)開(kāi)發(fā)(英語(yǔ):Behavior-driven development痹仙,縮寫(xiě)B(tài)DD)是一種敏捷軟件開(kāi)發(fā)的技術(shù)是尔,BDD的重點(diǎn)是通過(guò)與利益相關(guān)者的討論取得對(duì)預(yù)期的軟件行為的清醒認(rèn)識(shí)。它通過(guò)用自然語(yǔ)言書(shū)寫(xiě)非程序員可讀的測(cè)試用例擴(kuò)展了測(cè)試驅(qū)動(dòng)開(kāi)發(fā)方法开仰。

  • 測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(英語(yǔ):Test-driven development拟枚,縮寫(xiě)為T(mén)DD)是一種軟件開(kāi)發(fā)過(guò)程中的應(yīng)用方法,由極限編程中倡導(dǎo)抖所,以其倡導(dǎo)先寫(xiě)測(cè)試程序梨州,然后編碼實(shí)現(xiàn)其功能得名痕囱。測(cè)試驅(qū)動(dòng)開(kāi)發(fā)是戴兩頂帽子思考的開(kāi)發(fā)方式:先戴上實(shí)現(xiàn)功能的帽子田轧,在測(cè)試的輔助下,快速實(shí)現(xiàn)其功能鞍恢;再戴上重構(gòu)的帽子傻粘,在測(cè)試的保護(hù)下每窖,通過(guò)去除冗余的代碼,提高代碼質(zhì)量弦悉。測(cè)試驅(qū)動(dòng)著整個(gè)開(kāi)發(fā)過(guò)程:首先窒典,驅(qū)動(dòng)代碼的設(shè)計(jì)和功能的實(shí)現(xiàn);其后稽莉,驅(qū)動(dòng)代碼的再設(shè)計(jì)和重構(gòu)瀑志。

上面講述了TDD和BDD的思想差別,看到這里污秆,你們認(rèn)為當(dāng)前的iOS開(kāi)發(fā)適合怎樣的測(cè)試思想劈猪。不知道你們開(kāi)發(fā)中的實(shí)際情況是如何,在現(xiàn)在大環(huán)境趕進(jìn)度的開(kāi)發(fā)下良拼,一般我是采用BDD的測(cè)試方法战得。

而談到BDD,我要給大家介紹一個(gè)iOS中非常有名并且好用的BDD框架 —— Kiwi庸推。

Kiwi

Kiwi的安裝

使用Cocopods 安裝

target :YourProjectTests do
  pod 'Kiwi'
end

在這里記得一定要替換YourProject為你的項(xiàng)目名常侦。

Kiwi的基本結(jié)構(gòu)

在講Kiwi的常用語(yǔ)法前,我們先來(lái)看一段Kiwi的Github提供的示例代碼贬媒。

describe(@"Team", ^{
    context(@"when newly created", ^{
        it(@"should have a name", ^{
            id team = [Team team];
            [[team.name should] equal:@"Black Hawks"];
        });

        it(@"should have 11 players", ^{
            id team = [Team team];
            [[[team should] have:11] players];
        });
    });
});

我們很容易根據(jù)上下文將其提取為Given..When..Then的三段式自然語(yǔ)言聋亡。

Given a team, when newly created, it should have a name, and should have 11 players

是不是非常簡(jiǎn)單易懂的語(yǔ)法結(jié)構(gòu)。

describe描述需要測(cè)試的對(duì)象內(nèi)容际乘,也即我們?nèi)问街械?code>Given杀捻,context描述測(cè)試上下文,也就是這個(gè)測(cè)試在When來(lái)進(jìn)行蚓庭,最后it中的是測(cè)試的本體致讥,描述了這個(gè)測(cè)試應(yīng)該滿足的條件,三者共同構(gòu)成了Kiwi測(cè)試中的行為描述器赞。它們是可以nest的垢袱,也就是一個(gè)Spec文件中可以包含多個(gè)describe(雖然我們很少這么做,一個(gè)測(cè)試文件應(yīng)該專注于測(cè)試一個(gè)類)港柜;一個(gè)describe可以包含多個(gè)context请契,來(lái)描述類在不同情景下的行為;一個(gè)context可以包含多個(gè)it的測(cè)試?yán)?/p>

Kiwi還有一些其他的行為描述關(guān)鍵字夏醉,其中比較重要的包括:

  • beforeAll(aBlock) - 當(dāng)前scope內(nèi)部的所有的其他block運(yùn)行之前調(diào)用一次

  • afterAll(aBlock) - 當(dāng)前scope內(nèi)部的所有的其他block運(yùn)行之后調(diào)用一次

  • beforeEach(aBlock) - 在scope內(nèi)的每個(gè)it之前調(diào)用一次爽锥,對(duì)于context的配置代碼應(yīng)該寫(xiě)在這里

  • afterEach(aBlock) - 在scope內(nèi)的每個(gè)it之后調(diào)用一次,用于清理測(cè)試后的代碼

  • specify(aBlock) - 可以在里面直接書(shū)寫(xiě)不需要描述的測(cè)試

  • pending(aString, aBlock) - 只打印一條log信息畔柔,不做測(cè)試氯夷。這個(gè)語(yǔ)句會(huì)給出一條警告,可以作為一開(kāi)始集中書(shū)寫(xiě)行為描述時(shí)還未實(shí)現(xiàn)的測(cè)試的提示靶擦。

  • xit(aString, aBlock) - 和pending一樣腮考,另一種寫(xiě)法雇毫。因?yàn)樵谡嬲龑?shí)現(xiàn)時(shí)測(cè)試時(shí)只需要將x刪掉就是it,但是pending語(yǔ)意更明確踩蔚,因此還是推薦pending

Kiwi使用實(shí)例

就拿項(xiàng)目中一個(gè)真實(shí)的場(chǎng)景來(lái)說(shuō)棚放,我在寫(xiě)完一個(gè)適配所有iPhone機(jī)型的寬高的類之后,我用Kiwi來(lái)進(jìn)行單元測(cè)試馅闽。

首先我這個(gè)類是這么描述寬高的

//CalculateLayout.h

+ (CGFloat)neu_layoutForAlliPhoneHeight:(CGFloat)height;

+ (CGFloat)neu_layoutForAlliPhoneWidth:(CGFloat)width;

//  CalculateLayout.m

+ (CGFloat)layoutForAlliPhoneHeight:(CGFloat)height type:(IPhoneType)type {
    CGFloat layoutHeight = 0.0f;
    switch (type) {
        case iPhone4Type:
            layoutHeight = ( height / iPhone6Height ) * iPhone4Height;
            break;
        case iPhone5Type:
            layoutHeight = ( height / iPhone6Height ) * iPhone5Height;
            break;
        case iPhone6Type:
            layoutHeight = ( height / iPhone6Height ) * iPhone6Height;
            break;
        case iPhone6PlusType:
            layoutHeight = ( height / iPhone6Height ) * iPhone6PlusHeight;
            break;
        default:
            break;
    }
    return layoutHeight;
}

+ (CGFloat)layoutForAlliPhoneWidth:(CGFloat)width type:(IPhoneType)type {
    CGFloat layoutWidth = 0.0f;
    switch (type) {
        case iPhone4Type:
            layoutWidth = ( width / iPhone6Width ) * iPhone4Width;
            break;
        case iPhone5Type:
            layoutWidth = ( width / iPhone6Width ) * iPhone5Width;
            break;
        case iPhone6Type:
            layoutWidth = ( width / iPhone6Width ) * iPhone6Width;
            break;
        case iPhone6PlusType:
            layoutWidth = ( width / iPhone6Width ) * iPhone6PlusWidth;
            break;
        default:
            break;
    }
    return layoutWidth;
}

反正大概意思就是我輸入了一個(gè)寬高飘蚯,他根據(jù)UI給定的設(shè)計(jì)圖,返回給我一個(gè)寬高適配當(dāng)前機(jī)型的寬高福也。

那么我們?nèi)绾蝸?lái)寫(xiě)這個(gè)測(cè)試用例呢.

#import <Kiwi/Kiwi.h>
#import "CalculateLayout.h"


SPEC_BEGIN(CalculateLayoutTests)

describe(@"CalculateLayout", ^{
    context(@"when calculate width and height", ^{
        
        CGFloat width = [CalculateLayout neu_layoutForAlliPhoneWidth:375.f];
        CGFloat height = [CalculateLayout neu_layoutForAlliPhoneHeight:667.f];
        
        pending_(@"All iPhone Test", ^{
        });
        
        it(@"should layout width", ^{
            [[theValue(width) should] equal:theValue(320.f)];
        });
        
        it(@"should layout height", ^{
            [[theValue(height) should] equal:theValue(568.f)];
        });
    });
});

SPEC_END

我寫(xiě)進(jìn)去的寬高數(shù)值是iPhone6的寬高數(shù)值孝冒,如果用5S的模擬器來(lái)運(yùn)行,將會(huì)返回5S的寬高 320 * 568

當(dāng)我們 com+U 運(yùn)行這段測(cè)試用例時(shí)拟杉。

控制臺(tái)的輸出

+ 'CalculateLayout, when calculate width and height, should layout width' [PASSED]

+ 'CalculateLayout, when calculate width and height, should layout height' [PASSED]

可以看到庄涡,由于有context的存在,以及其可以嵌套的特性搬设,測(cè)試的流程控制相比傳統(tǒng)測(cè)試可以更加精確穴店。我們更容易把beforeafter的作用區(qū)域限制在合適的地方。

實(shí)際的測(cè)試寫(xiě)在it里拿穴,是由一個(gè)一個(gè)的期望(Expectations)來(lái)進(jìn)行描述的泣洞,期望相當(dāng)于傳統(tǒng)測(cè)試中的斷言,要是運(yùn)行的結(jié)果不能匹配期望默色,則測(cè)試失敗球凰。在Kiwi中期望都由should或者shouldNot開(kāi)頭,并緊接一個(gè)或多個(gè)判斷的的鏈?zhǔn)秸{(diào)用腿宰,大部分常見(jiàn)的是be或者haveSomeCondition的形式呕诉。在我們上面的例子中我們使用了should not be nilshould equal兩個(gè)期望來(lái)確保字符串賦值的行為正確。其他的期望語(yǔ)句非常豐富吃度,并且都符合自然語(yǔ)言描述甩挫,所以并不需要太多介紹。在使用的時(shí)候不妨直接按照自己的想法來(lái)描述自己的期望椿每,一般情況下在IDE的幫助下我們都能找到想要的結(jié)果伊者。如果您想看看完整的期望語(yǔ)句的列表,可以參看文檔的這個(gè)頁(yè)面间护。從這一點(diǎn)來(lái)看亦渗,Kiwi可以說(shuō)是一個(gè)非常靈活并具有可擴(kuò)展性的測(cè)試框架。

來(lái)解釋下上面的語(yǔ)法中用到的theValue.

Kiwi為我們提供了一個(gè)標(biāo)量轉(zhuǎn)對(duì)象的語(yǔ)法糖汁尺,叫做theValue法精,在做精確比較的時(shí)候我們可以直接使用例子中直接與320.f或者568.f做比較這樣的寫(xiě)法來(lái)進(jìn)行對(duì)比。

通過(guò)這樣一個(gè)簡(jiǎn)單的例子,我們基本能掌握Kiwi的語(yǔ)法亿虽,以及Kiwi的使用菱涤。單元測(cè)試的門(mén)其實(shí)很好進(jìn)苞也,但是如何用心的洛勉,動(dòng)腦子的去寫(xiě)單元測(cè)試,則是對(duì)我們程序員莫大的考驗(yàn)哦如迟。

我講的并不完善收毫,也不詳細(xì),就算簡(jiǎn)單記錄自己目前的收獲吧殷勘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末此再,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子玲销,更是在濱河造成了極大的恐慌输拇,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贤斜,死亡現(xiàn)場(chǎng)離奇詭異策吠,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)瘩绒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)猴抹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人锁荔,你說(shuō)我怎么就攤上這事蟀给。” “怎么了阳堕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵跋理,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我恬总,道長(zhǎng)薪介,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任越驻,我火速辦了婚禮汁政,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缀旁。我一直安慰自己记劈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布并巍。 她就那樣靜靜地躺著目木,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上刽射,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天军拟,我揣著相機(jī)與錄音,去河邊找鬼誓禁。 笑死懈息,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的摹恰。 我是一名探鬼主播辫继,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼俗慈!你這毒婦竟也來(lái)了姑宽?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤闺阱,失蹤者是張志新(化名)和其女友劉穎炮车,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體酣溃,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瘦穆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了救拉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片难审。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖亿絮,靈堂內(nèi)的尸體忽然破棺而出告喊,到底是詐尸還是另有隱情,我是刑警寧澤派昧,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布黔姜,位于F島的核電站,受9級(jí)特大地震影響蒂萎,放射性物質(zhì)發(fā)生泄漏秆吵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一五慈、第九天 我趴在偏房一處隱蔽的房頂上張望纳寂。 院中可真熱鬧,春花似錦泻拦、人聲如沸毙芜。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)腋粥。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間隘冲,已是汗流浹背闹瞧。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留展辞,地道東北人奥邮。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像纵竖,于是被迫代替她去往敵國(guó)和親漠烧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子杏愤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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

  • 我們?yōu)槭裁匆脺y(cè)試框架呢靡砌?當(dāng)然對(duì)項(xiàng)目開(kāi)發(fā)有幫助了,但是業(yè)內(nèi)現(xiàn)狀是經(jīng)常趕進(jìn)度珊楼,所以TDD還是算了吧通殃,BDD就測(cè)測(cè)數(shù)據(jù)...
    CrespoXiao閱讀 14,408評(píng)論 9 60
  • 哭鬧著要走的人,都不會(huì)真正離開(kāi)厕宗。真正想要離開(kāi)的那個(gè)人画舌,挑一個(gè)風(fēng)和日麗的下午,穿上一件大衣出門(mén)已慢,消失在秋日的陽(yáng)光里曲聂,...
    苦行尼閱讀 179評(píng)論 0 0
  • 這幾天有些頹廢、迷茫佑惠,日記好些天沒(méi)有寫(xiě)了朋腋,今天七夕;做好了十幾枚竼文佛像印章膜楷。 其實(shí)這幾天也沒(méi)有特別忙旭咽,就是老毛病...
    王益軍閱讀 203評(píng)論 0 0
  • 1. 環(huán)境 Windows 7 Visual Studio 2017 2. Build OPC UA-.NET 1...
    YZUA閱讀 5,519評(píng)論 0 2