BDD系列一:Specta

上篇文章系统性地介绍了BDD,那么从这篇文章开始逐步实践BDD,首先从BDD框架开始。

介绍

从iOS测试与集成工具总结中了解到:

  • Kiwi是对XCTest的一个完整替代,使用xSpec风格编写测试。Kiwi带有自己的一套工具集,包括expectations、mocks、stubs,甚至还支持异步测试。

  • SpectaKiwi功能相似,但在架构上非常不同。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再结合OCMockExpectaOHHTTPStubs等框架会让你事半功倍。
在编写测试过程中,能够反推你去设计程序:应避免暴露内部实现;使用依赖注入利于模块化代码结构;把关键事件抽象出来组件化等。

你可能感兴趣的:(BDD系列一:Specta)