上篇文章系统性地介绍了BDD,那么从这篇文章开始逐步实践BDD,首先从BDD框架开始。
介绍
从iOS测试与集成工具总结中了解到:
Kiwi
是对XCTest的一个完整替代,使用xSpec风格编写测试。Kiwi带有自己的一套工具集,包括expectations、mocks、stubs,甚至还支持异步测试。Specta
与Kiwi
功能相似,但在架构上非常不同。Kiwi注重功能的整合,而Specta则注重模块化。它本身只专注于运行测试,而将模拟、匹配等功能交给第三方。
而在我的实际项目也是注重模块化,那么就从Specta开始。
Specta的DSL
都在SpectaDSL.h
中,而在Specta的EXAMPLE
中也可以看到具体的示例,以及相应DSL
的介绍。
常用的DSL:
-
SpecBegin
声明了一个名为xx的测试类; -
SpecEnd
结束了类声明; -
describe
声明了一组实例; -
context
的行为类似于describe(语法糖); -
it
是一个单一的例子 (单一测试); -
beforeEach
是一个运行于所有同级block和嵌套block之前的block; -
afterEach
是一个运行于所有同级block和嵌套block之后的block。
案例实践
格式化字符串
在项目中难免会遇到需要把几个字符串拼接并按特定格式输出,比如一条消费信息:
并要求:
如果没有优惠就不显示优惠信息
。
首先需要知道数据模型:
@interface ConsumeInfo : NSObject
// 商家名称
@property (nonatomic, readonly) NSString *merchantName;
// 消费金额
@property (nonatomic, readonly) NSString *spendPrice;
// 优惠金额
@property (nonatomic, readonly) NSString *discountPrice;
@end
至于格式化具体实现封装在EventDescriptionFormatter
类中,只需暴露一个方法:
@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数据
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(@"海底捞(海岸城店) -消费:¥880.00 -优惠:¥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(@"海底捞(海岸城店) -消费:¥880.00");
});
});
});
SpecEnd
上面测试用例就是关于消费信息格式化的,其中用了OCMock(模拟测试框架)和Expecta(匹配程序框架),这两个框架会在后续文章中具体介绍。
资料信息提交
提交资料信息,首先需要填写或者选择信息。比如,绑定银行卡,就需要填写银行卡号、银行名称(一般根据银行卡号得出)、银行预留手机号。
首先把负责提交资料的组件抽象到一个称为BankInfoCommitApi
的类中,只需暴露一个方法:
@interface BankInfoCommitApi : NSObject
- (void)commitWithBankName:(NSString *)bankName bankCard:(NSString *)bankCard mobile:(NSString *)mobile;
@end
接着思考如何获取控制器中的UI控件,我们可以使用一个分类:
@interface UIView (Specs)
- (UIButton *)specsFindButtonWithTitle:(NSString *)title;
- (UITextField *)specsFindTextFieldWithPlaceholder:(NSString *)placeholder;
- (UILabel *)specsFindLabelWithText:(NSString *)text;
@end
再就是模拟点击事件,也使用分类解决:
@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 设置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:@"预留手机号"];
commitButton = [view specsFindButtonWithTitle:@"提交"];
});
context(@"when all info are present", ^{
beforeEach(^{
bankNameTextField.text = @"建设银行";
bankCardTextField.text = @"43123546576887066";
mobileTextField.text = @"13813800012";
[commitButton specsSimulateTap];
});
it(@"should response commit bank info method", ^{
[[mockBankInfoCommitApi expect] commitWithBankName:@"建设银行" bankCard:@"43123546576887066" mobile:@"13813800012"];
[mockBankInfoCommitApi verify];
});
});
context(@"when one of bank info are not present", ^{
beforeEach(^{
bankNameTextField.text = @"建设银行";
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
这个测试用例用于测试:当点击提交按钮,如果银行信息都全的话,响应commitWithBankName:bankCard:mobile:
方法,如果不全就不响应。
总结
牢记:测试对象的行为方式,使用Specta
再结合OCMock
、Expecta
、OHHTTPStubs
等框架会让你事半功倍。
在编写测试过程中,能够反推你去设计程序:应避免暴露内部实现;使用依赖注入利于模块化代码结构;把关键事件抽象出来组件化等。