BDD系列一:Specta

上篇文章系統(tǒng)性地介紹了BDD,那么從這篇文章開始逐步實(shí)踐BDD词爬,首先從BDD框架開始秃嗜。

介紹

iOS測試與集成工具總結(jié)中了解到:

  • Kiwi是對XCTest的一個(gè)完整替代,使用xSpec風(fēng)格編寫測試顿膨。Kiwi帶有自己的一套工具集痪寻,包括expectations、mocks虽惭、stubs橡类,甚至還支持異步測試。

  • SpectaKiwi功能相似芽唇,但在架構(gòu)上非常不同顾画。Kiwi注重功能的整合,而Specta則注重模塊化匆笤。它本身只專注于運(yùn)行測試研侣,而將模擬、匹配等功能交給第三方炮捧。

而在我的實(shí)際項(xiàng)目也是注重模塊化庶诡,那么就從Specta開始。

Specta的DSL都在SpectaDSL.h中咆课,而在SpectaEXAMPLE中也可以看到具體的示例末誓,以及相應(yīng)DSL的介紹。

常用的DSL:

  • SpecBegin聲明了一個(gè)名為xx的測試類书蚪;
  • SpecEnd結(jié)束了類聲明喇澡;
  • describe聲明了一組實(shí)例;
  • context的行為類似于describe(語法糖)殊校;
  • it是一個(gè)單一的例子 (單一測試)晴玖;
  • beforeEach是一個(gè)運(yùn)行于所有同級block和嵌套block之前的block;
  • afterEach是一個(gè)運(yùn)行于所有同級block和嵌套block之后的block为流。

案例實(shí)踐

格式化字符串

在項(xiàng)目中難免會遇到需要把幾個(gè)字符串拼接并按特定格式輸出呕屎,比如一條消費(fèi)信息:


并要求:如果沒有優(yōu)惠就不顯示優(yōu)惠信息

首先需要知道數(shù)據(jù)模型:

@interface ConsumeInfo : NSObject
// 商家名稱
@property (nonatomic, readonly) NSString *merchantName;
// 消費(fèi)金額
@property (nonatomic, readonly) NSString *spendPrice;
// 優(yōu)惠金額
@property (nonatomic, readonly) NSString *discountPrice;

@end

至于格式化具體實(shí)現(xiàn)封裝在EventDescriptionFormatter類中敬察,只需暴露一個(gè)方法:

@interface EventDescriptionFormatter : NSObject

- (NSString *)eventDescriptionFromConsumeInfo:(id)consumeInfo;

@end

下面開始寫測試用例:

SpecBegin(EventDescriptionFormatter)

describe(@"consume info description", ^{
   __block NSString *eventDescription;
   __block id mockEvent;
   __block EventDescriptionFormatter *descriptionFormatter;
   
   beforeEach(^{
       descriptionFormatter = [[EventDescriptionFormatter alloc]init];
   });
   
   context(@"when discountPrice are present", ^{
       beforeEach(^{
           //mock數(shù)據(jù)
           mockEvent = [OCMockObject mockForClass:[ConsumeInfo class]];
           [(ConsumeInfo *)[[mockEvent stub] andReturn:@"海底撈(海岸城店)"] merchantName];
           [(ConsumeInfo *)[[mockEvent stub] andReturn:@"880.00"] spendPrice];
           [(ConsumeInfo *)[[mockEvent stub] andReturn:@"2.88"] discountPrice];
           eventDescription = [descriptionFormatter eventDescriptionFromConsumeInfo:mockEvent];
       });
       it(@"should return formatted description", ^{
           expect(eventDescription).to.equal(@"海底撈(海岸城店) -消費(fèi):¥880.00 -優(yōu)惠:¥2.88");
       });
   });
    
   context(@"when discountPrice are not present", ^{
       beforeEach(^{
           mockEvent = [OCMockObject mockForClass:[ConsumeInfo class]];
           [(ConsumeInfo *)[[mockEvent stub] andReturn:@"海底撈(海岸城店)"] merchantName];
           [(ConsumeInfo *)[[mockEvent stub] andReturn:@"880.00"] spendPrice];
           [(ConsumeInfo *)[[mockEvent stub] andReturn:nil] discountPrice];
           eventDescription = [descriptionFormatter eventDescriptionFromConsumeInfo:mockEvent];
       });
       it(@"should return formatted description", ^{
           expect(eventDescription).to.equal(@"海底撈(海岸城店) -消費(fèi):¥880.00");
       });
   });   
});
SpecEnd

上面測試用例就是關(guān)于消費(fèi)信息格式化的秀睛,其中用了OCMock(模擬測試框架)和Expecta(匹配程序框架),這兩個(gè)框架會在后續(xù)文章中具體介紹静汤。

資料信息提交

提交資料信息琅催,首先需要填寫或者選擇信息。比如虫给,綁定銀行卡藤抡,就需要填寫銀行卡號、銀行名稱(一般根據(jù)銀行卡號得出)抹估、銀行預(yù)留手機(jī)號缠黍。

首先把負(fù)責(zé)提交資料的組件抽象到一個(gè)稱為BankInfoCommitApi的類中,只需暴露一個(gè)方法:

@interface BankInfoCommitApi : NSObject

- (void)commitWithBankName:(NSString *)bankName bankCard:(NSString *)bankCard mobile:(NSString *)mobile;

@end

接著思考如何獲取控制器中的UI控件药蜻,我們可以使用一個(gè)分類:

@interface UIView (Specs)

- (UIButton *)specsFindButtonWithTitle:(NSString *)title;

- (UITextField *)specsFindTextFieldWithPlaceholder:(NSString *)placeholder;

- (UILabel *)specsFindLabelWithText:(NSString *)text;

@end

再就是模擬點(diǎn)擊事件瓷式,也使用分類解決:

@implementation UIButton (Specs)

- (void)specsSimulateTap{
  [self sendActionsForControlEvents:UIControlEventTouchUpInside];
}

@end

好了,下面開始編寫測試用例:

SpecBegin(ViewController)

describe(@"viewController", ^{
    __block ViewController *viewController;
    __block id mockBankInfoCommitApi;
    
    beforeEach(^{
        mockBankInfoCommitApi = [OCMockObject mockForClass:[BankInfoCommitApi class]];
        viewController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"ViewController"];
        
        // 使用KVC 設(shè)置viewController 的api為 mockBankInfoCommitApi
        [viewController setValue:mockBankInfoCommitApi forKey:@"api"];
    });
    
    afterEach(^{
        viewController = nil;
    });
    
    describe(@"view", ^{
        
        __block UIView *view;
        
        beforeEach(^{
            view = [viewController view];
        });
        
        describe(@"commit button", ^{
            __block UITextField *bankNameTextField;
            __block UITextField *bankCardTextField;
            __block UITextField *mobileTextField;
            __block UIButton *commitButton;
            
            beforeEach(^{
                bankNameTextField = [view specsFindTextFieldWithPlaceholder:@"銀行名稱"];
                bankCardTextField = [view specsFindTextFieldWithPlaceholder:@"銀行卡號"];
                mobileTextField = [view specsFindTextFieldWithPlaceholder:@"預(yù)留手機(jī)號"];
                commitButton = [view specsFindButtonWithTitle:@"提交"];
            });
            
            context(@"when all info are present", ^{
                beforeEach(^{
                    bankNameTextField.text = @"建設(shè)銀行";
                    bankCardTextField.text = @"43123546576887066";
                    mobileTextField.text = @"13813800012";
            
                    [commitButton specsSimulateTap];
                });
                
                it(@"should response commit bank info method", ^{
                    
                    [[mockBankInfoCommitApi expect] commitWithBankName:@"建設(shè)銀行" bankCard:@"43123546576887066" mobile:@"13813800012"];
                    
                    [mockBankInfoCommitApi verify];
                });
            });
            
            context(@"when one of bank info are not present", ^{
                beforeEach(^{
                    bankNameTextField.text = @"建設(shè)銀行";
                    bankCardTextField.text = @"43123546576887066";
                    mobileTextField.text = @"";
                    
                    [commitButton specsSimulateTap];
                });
                it(@"should not response commit bank info method", ^{

                    [[mockBankInfoCommitApi reject] commitWithBankName:[OCMArg any] bankCard:[OCMArg any] mobile:[OCMArg any]];
                
                    [mockBankInfoCommitApi verify];
                });
            });
        });
    });
});

SpecEnd

這個(gè)測試用例用于測試:當(dāng)點(diǎn)擊提交按鈕语泽,如果銀行信息都全的話贸典,響應(yīng)commitWithBankName:bankCard:mobile:方法,如果不全就不響應(yīng)踱卵。

總結(jié)

牢記:測試對象的行為方式廊驼,使用Specta再結(jié)合OCMockExpecta惋砂、OHHTTPStubs等框架會讓你事半功倍妒挎。
在編寫測試過程中,能夠反推你去設(shè)計(jì)程序:應(yīng)避免暴露內(nèi)部實(shí)現(xiàn)西饵;使用依賴注入利于模塊化代碼結(jié)構(gòu)酝掩;把關(guān)鍵事件抽象出來組件化等。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末眷柔,一起剝皮案震驚了整個(gè)濱河市期虾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驯嘱,老刑警劉巖彻消,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宙拉,居然都是意外死亡宾尚,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門谢澈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來煌贴,“玉大人,你說我怎么就攤上這事锥忿∨V#” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵敬鬓,是天一觀的道長淹朋。 經(jīng)常有香客問我笙各,道長,這世上最難降的妖魔是什么础芍? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任杈抢,我火速辦了婚禮,結(jié)果婚禮上仑性,老公的妹妹穿的比我還像新娘惶楼。我一直安慰自己,他們只是感情好诊杆,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布歼捐。 她就那樣靜靜地躺著,像睡著了一般晨汹。 火紅的嫁衣襯著肌膚如雪豹储。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天淘这,我揣著相機(jī)與錄音颂翼,去河邊找鬼。 笑死慨灭,一個(gè)胖子當(dāng)著我的面吹牛朦乏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播氧骤,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼呻疹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了筹陵?” 一聲冷哼從身側(cè)響起刽锤,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎朦佩,沒想到半個(gè)月后并思,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡语稠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年宋彼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仙畦。...
    茶點(diǎn)故事閱讀 38,646評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡输涕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出慨畸,到底是詐尸還是另有隱情莱坎,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布寸士,位于F島的核電站檐什,受9級特大地震影響碴卧,放射性物質(zhì)發(fā)生泄漏骇塘。R本人自食惡果不足惜沐序,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一瘦癌、第九天 我趴在偏房一處隱蔽的房頂上張望檬洞。 院中可真熱鬧,春花似錦荷辕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至划栓,卻和暖如春兑巾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背忠荞。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工蒋歌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人委煤。 一個(gè)月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓堂油,卻偏偏與公主長得像,于是被迫代替她去往敵國和親碧绞。 傳聞我的和親對象是個(gè)殘疾皇子府框,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評論 2 348

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

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件讥邻、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,064評論 4 62
  • 雨落春花 飛逝兩冬夏 往日情話 盡是漫天舞黃沙 浮沉如畫 蒼蒼蒹葭 念如君風(fēng)華 幸得當(dāng)下 可仗劍執(zhí)戟披甲 縱馬天涯...
    水中禾閱讀 277評論 0 1
  • 目錄:不長不短兴使,剛好刻骨銘心 上一章:夢的另一個(gè)世界 文/陳康慧 夏琉璃喃喃自語的重復(fù)著:“夢系宜?另一個(gè)我?” 二丁...
    陳康慧閱讀 330評論 14 18
  • 小組構(gòu)建地域:最優(yōu)為北京地區(qū) 小組構(gòu)成優(yōu)選:希望除產(chǎn)品經(jīng)理以外发魄,可以有程序盹牧、設(shè)計(jì)資源的加盟 選題:【方寸之間】第三...
    貓本游閱讀 774評論 0 50
  • BaiXiang的CRNN論文閱讀 1. 論文題目 BaiXiang——【arXiv2015】An End-to-...
    lillycao閱讀 9,186評論 2 1