上篇文章系統(tǒng)性地介紹了BDD,那么從這篇文章開始逐步實(shí)踐BDD词爬,首先從BDD框架開始秃嗜。
介紹
從iOS測試與集成工具總結(jié)中了解到:
Kiwi
是對XCTest的一個(gè)完整替代,使用xSpec風(fēng)格編寫測試顿膨。Kiwi帶有自己的一套工具集痪寻,包括expectations、mocks虽惭、stubs橡类,甚至還支持異步測試。Specta
與Kiwi
功能相似芽唇,但在架構(gòu)上非常不同顾画。Kiwi注重功能的整合,而Specta則注重模塊化匆笤。它本身只專注于運(yùn)行測試研侣,而將模擬、匹配等功能交給第三方炮捧。
而在我的實(shí)際項(xiàng)目也是注重模塊化庶诡,那么就從Specta開始。
Specta的DSL
都在SpectaDSL.h
中咆课,而在Specta的EXAMPLE
中也可以看到具體的示例末誓,以及相應(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é)合OCMock
、Expecta
惋砂、OHHTTPStubs
等框架會讓你事半功倍妒挎。
在編寫測試過程中,能夠反推你去設(shè)計(jì)程序:應(yīng)避免暴露內(nèi)部實(shí)現(xiàn)西饵;使用依賴注入利于模塊化代碼結(jié)構(gòu)酝掩;把關(guān)鍵事件抽象出來組件化等。