表现让开发iOS如何在ReactiveCocoa中描写单元测试。

Designer News.png

今天游人如织人口于付出iOS时犹利用ReactiveCocoa,它是一个函数式和响应式编程的框架,使用Signal来代替KVO、Notification、Delegate和Target-Action等传递信息及化解对象之间状态及状态的指过多问题。但多时节下其后,如何编写单元测试来证明程序是否正确吧?下面首先了解MVVM架构,然后经一个例子来叙述自己什么在RAC(ReactiveCocoa简称)中使用Kiwi来修单元测试。

前段时间在design+code购入了一个学习iOS设计以及编码在线课程,使用Sketch设计App,然后用Swift语言实现Designer
News客户端。作者Meng
To曾开源及Github:MengTo/DesignerNewsApp ·
GitHub。虽然实现普Designer
News客户端基本功能,但是使用臃肿MVC(Model-View-Controller)架构,不易为代码的测试和复用,于是用ReactiveCocoa兑现MVVM(Model-View-View
Model)架构,加上一个用Objective-C实现的BDD测试框架Kiwi来单元测试,就可作为令开发iOS
App。

MVVM架构

MVVM high level

于MVVM架构中,通常都用view和view
controller看做一个整体。相对于前MVC架构中view
controller执行很多每当view和model之间数据映射和互动的工作,现在用她交给view
model去开。
至于选择哪种机制来更新view
model或view是不曾强制的,但常见咱们且选ReactiveCocoa。ReactiveCocoa会监听model的更动然后将这些反映射到view
model的特性被,并且可实施有事务逻辑。

选个例来说,有一个model包含一个dateAdded的特性,我眷恋监听它的变动然后更新view
model的dateAdded属性。但model的dateAdded属性的数据类型是NSDate,而view
model的数据类型是NSString,所以当view
model的init方法吃进行数量绑定,但需要数类型转换。示例代码如下:

RAC(self,dateAdded) = [RACObserve(self.model,dateAdded) map:^(NSDate*date){ 
    return [[ViewModel dateFormatter] stringFromDate:date];
}];

ViewModel调用dateFormatter进行数量易,且方法dateFormatter可以复用到另外地方。然后view
controller监听view model的dateAdded属性且绑定到label的text属性。

RAC(self.label,text) = RACObserve(self.viewModel,dateAdded);

现今咱们抽象出日期变到字符串的逻辑到view
model,使得代码可以测试复用,并且帮view controller瘦身

ReactiveCocoa

ReactiveCocoa是一个用Objective-C编写,具有函数式和响应式特性的编程框架。大多数之开发者他们排忧解难问题的思索方式都是安好任务,通常的做法即是编制很多发令,然后修改要数据结构的状态,这种编程范式叫做命令式编程(Imperative
Programming)。与命令式编程不同的凡函数式编程(Functional
Programming),思考问题的计是好什么任务,怎样描述是职责。关于对函数式编程入门概念的知情,可以参考酷壳《函数式编程》这首文章,深入浅出对函数式编程的思索方式、特性以及技术通过有些示范来上课。

报到现象

大红鹰葡京会 1

报到现象

如图所示,这是一个简易的签到界面:有用户称与密码的有限独输入框,一个登录按钮。用户输入完用户名和密码后,点击登录按钮后,成功登录。但此间有克法:用户称必须满足邮件的格式和密码长度要以6各类以上。当以满足这简单单原则后才能够点击按钮,否则按钮是不足点击的。大家可以从github中下载实例代码。

首先我们事先画界面,我定义一个LoginView,将绘登录界面的义务都交其。然后以LoginViewController中的viewDidLoad办法调用buildViewHierarchy加载它

#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];

    // build view hierarchy
    [self buildViewHierarchy];
    // bind data
    [self bindData];
    // handle events
    [self handleEvents];
}

- (void)buildViewHierarchy
{
    [self.view addSubview:self.rootView];
    [self.rootView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
}

连下去我们要思考UI如何相互与如何计划以及实现怎样类来拍卖。由于用户称与密码要又满足验证格式时才会点击登录按钮,所以要时刻监听usernameTextFieldpasswordTextField的text属性,对于拍卖UI交互、数据校验以及转换都交由MVVM架构中ViewModel来处理。于是定义一个LoginViewModel,并继承RVMViewModel,这个RVMViewModel有个active性来代表viewModel是否处在活跃状态,当active是YES时,更新或显示UI。当active是NO时,不更新或隐藏UI。

@interface LoginViewModel : RVMViewModel

#pragma mark - UI state
/*
 @brief 用户名
 */
@property (copy, nonatomic) NSString *username;
/*
 @brief 密码
 */
@property (copy, nonatomic) NSString *password;

#pragma mark - Handle events
/*
 @brief 处理用户民和密码是否有效才能点击按钮以及登陆事件
 */
@property (nonatomic, strong) RACCommand *loginCommand;

#pragma mark - Methods
- (RACSignal *)isValidUsernameAndPasswordSignal;

@end

上面还有一个loginCommand属性和isValidUsernameAndPasswordSignal艺术齐下会详细介绍。定义LoginViewModel类后,在LoginViewController构成以及委托的点子来利用LoginViewModel并使用Lazy
Initialization
来初始化它。

@interface LoginViewController ()

#pragma mark - View model
@property (strong, nonatomic) LoginViewModel *loginViewModel;

@end

@implementation LoginViewController

#pragma mark - Custom Accessors
- (LoginViewModel *)loginViewModel
{
    if (!_loginViewModel) {
        _loginViewModel = [LoginViewModel new];
    }
    return _loginViewModel;
}

末调用bindData主意开展数量绑定

- (void)bindData
{
    RAC(self.loginViewModel, username) = self.rootView.usernameTextField.rac_textSignal;
    RAC(self.loginViewModel, password) = self.rootView.passwordTextField.rac_textSignal;
}

ReactiveCocoa解决什么问题?

  • 目标中状态与状态的指过多问题
    借用ReactiveCocoa中一个事例来证实:用户以登录界面时,有一个用户称输入框和密码输入框,还有一个登录按钮。登录交互要求如下:

  • 当用户称及密码可验证格式,并且之前还并未登录时,登录按钮才能够点击。

  • 当点击登录成功登录后,设置已报到状态。

民俗的做法代码如下:

static void *ObservationContext = &ObservationContext;

- (void)viewDidLoad {
   [super viewDidLoad];

   [LoginManager.sharedManager addObserver:self forKeyPath:@"loggingIn" options:NSKeyValueObservingOptionInitial context:&ObservationContext];
   [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(loggedOut:) name:UserDidLogOutNotification object:LoginManager.sharedManager];

   [self.usernameTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged];
   [self.passwordTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged];
   [self.logInButton addTarget:self action:@selector(logInPressed:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)dealloc {
   [LoginManager.sharedManager removeObserver:self forKeyPath:@"loggingIn" context:ObservationContext];
   [NSNotificationCenter.defaultCenter removeObserver:self];
}

- (void)updateLogInButton {
   BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0;
   BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn;
   self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn;
}

- (IBAction)logInPressed:(UIButton *)sender {
   [[LoginManager sharedManager]
       logInWithUsername:self.usernameTextField.text
       password:self.passwordTextField.text
       success:^{
           self.loggedIn = YES;
       } failure:^(NSError *error) {
           [self presentError:error];
       }];
}

- (void)loggedOut:(NSNotification *)notification {
   self.loggedIn = NO;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
   if (context == ObservationContext) {
       [self updateLogInButton];
   } else {
       [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
   }
}

以上使用KVO、Notification、Target-Action等处理事件或信息之计编写的代码分散到各个地方,变得乱七八糟和不便明白;但是以RACSignal统一处理的话,代码更加简明和易读。使用RAC后代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    @weakify(self);

    RAC(self.logInButton, enabled) = [RACSignal
        combineLatest:@[
            self.usernameTextField.rac_textSignal,
            self.passwordTextField.rac_textSignal,
            RACObserve(LoginManager.sharedManager, loggingIn),
            RACObserve(self, loggedIn)
        ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) {
            return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
        }];

    [[self.logInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) {
        @strongify(self);

        RACSignal *loginSignal = [LoginManager.sharedManager
            logInWithUsername:self.usernameTextField.text
            password:self.passwordTextField.text];

            [loginSignal subscribeError:^(NSError *error) {
                @strongify(self);
                [self presentError:error];
            } completed:^{
                @strongify(self);
                self.loggedIn = YES;
            }];
    }];

    RAC(self, loggedIn) = [[NSNotificationCenter.defaultCenter
        rac_addObserverForName:UserDidLogOutNotification object:nil]
        mapReplace:@NO];
}
  • 人情MVC架构中,由于Controller承担数据印证、映射数据模型到View和操作View层次结构等多个责任,导致Controller过于臃肿,不便利代码的复用和测试。
    当传统的MVC架构中,主要出Model,
    View和Controller三部分组成。Model主要是保留数据与处理业务逻辑,View将数据显示,而Controller调解关于Model和View之间的有着交互。
    当数码到时,Model通过Key-Value Observation来打招呼View Controller,
    然后View Controller更新View。当View与用户交互后,View
    Controller更新Model。

Typical MVC paradigm.png

正要而你所展现,View
Controller隐式承担多责:数据说明、映射数据模型到View和操作View层次结构。MVVM将广大逻辑从View
Controller移走及View-Model,等引见完ReactiveCocoa后会见介绍MVVM架构。还有一对有关如何减负View
Controller好文章请参考objc中国再轻量的View
Controllers系列:

  • 又轻量的 View
    Controllers

  • 整洁的 Table View
    代码

  • 测试 View
    Controllers

  • 下Signal来替代KVO、Notification、Delegate和Target-Action等传递信息
    iOS开发中发出多种消息传递方式,KVO、Notification、Delegate、Block和Target-Action,对于它中产生啊异样以及哪些选要参见《消息传递机制》。但RAC提供RACSignal来统一消息传递机制,不再为争抉择何种传递信息方式若闹心。

    RAC对常用UI控件事件进展封装成一个RACSignal对象,以便对发出的各种风波开展监听。
    KVO示例代码如下:

// When self.username changes, logs the new name to the console.
//
// RACObserve(self, username) creates a new RACSignal that sends the current
// value of self.username, then the new value whenever it changes.
// -subscribeNext: will execute the block whenever the signal sends a value.
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

Target-Action示例代码如下:

// Logs a message whenever the button is pressed.
//
// RACCommand creates signals to represent UI actions. Each signal can
// represent a button press, for example, and have additional work associated
// with it.
//
// -rac_command is an addition to NSButton. The button will send itself on that
// command whenever it's pressed.
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
    NSLog(@"button was pressed!");
    return [RACSignal empty];
}];

Notification示例代码如下:

 // Respond to when email text start and end editing
 [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidBeginEditingNotification object:self.emailTextField] subscribeNext:^(id x) {
      [self.emailImageView animate];
      self.emailImageView.image = [UIImage imageNamed:@"icon-mail-active"];
      self.emailTextField.background = [UIImage imageNamed:@"input-outline-active"];
  }];

 [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidEndEditingNotification object:self.emailTextField] subscribeNext:^(id x) {
      self.emailTextField.background = [UIImage imageNamed:@"input-outline"];
      self.emailImageView.image = [UIImage imageNamed:@"icon-mail"];
  }];

除此之外,还足以动用AFNetworking走访服务器后针对回数据从创立一个RACSignal。示例代码如下:

 + (RACSubject*)storiesForSection:(NSString*)section page:(NSInteger)page
{
    RACSubject* signal = [RACSubject subject];

    NSDictionary* parameters = @{
        @"page" : [NSString stringWithFormat:@"%ld", (long)page],
        @"client_id" : clientID
    };

    [[AFHTTPSessionManager manager] GET:[DesignerNewsURL stroiesURLString] parameters:parameters success:^(NSURLSessionDataTask* task, id responseObject) {
                NSLog(@"url string = %@", task.currentRequest.URL);
                [signal sendNext:responseObject];
                [signal sendCompleted];
    } failure:^(NSURLSessionDataTask* task, NSError* error) {
                NSLog(@"url string = %@", task.currentRequest.URL);
                [signal sendError:error];
    }];

    return signal;
}

稍稍朋友可以发稍意想不到,上面代码明明返回的凡RACSubject,而非是RACSignal,其实RACSubject是RACSignal的子类,但是RACSubject写来代码更加简洁,所以使用RACSubject(官方不推荐应用)。等下用RAC核心类设计时,你不怕会了解它们中的涉及和怎样选择。

数绑定测试

苟usernameTextField.text、passwordTextField.text与loginViewModel.username、loginViewModel.password已经绑定数据,那么usernameTextField.text和passwordTextField.text的多少变动的话,一定会惹loginViewModel.username和loginViewModel.password的转。那么测试用例足这么设计:

数绑定 Test Case

因此kiwi编写测试如下:

SPEC_BEGIN(LoginViewControllerSpec)

describe(@"LoginViewController", ^{
    __block LoginViewController *controller = nil;

    beforeEach(^{
        controller = [LoginViewController new];
        [controller view];
    });

    afterEach(^{
        controller = nil;
    });

    describe(@"Root View", ^{
        __block LoginView *rootView = nil;

        beforeEach(^{
            rootView = controller.rootView;
        });

        context(@"when view did load", ^{
            it(@"should bind data", ^{
                rootView.usernameTextField.text = @"samlau";
                rootView.passwordTextField.text = @"freedom";

                [rootView.usernameTextField sendActionsForControlEvents:UIControlEventEditingChanged];
                [rootView.passwordTextField sendActionsForControlEvents:UIControlEventEditingChanged];

                [[controller.loginViewModel.username should] equal:rootView.usernameTextField.text];
                [[controller.loginViewModel.password should] equal:rootView.passwordTextField.text];
            });
        });

    });
});

SPEC_END

此测试中生出两点消重点说明:

  • 初始化完controller之后,controller自然要是调用view计来加载controller的view,否则不会见调用viewDidLoad方法。

比方小朋友对controller如何管理view生命周期不打听,可以看View
Controller Programming Guide for
iOS文档中的A
View Controller Instantiates Its View Hierarchy When Its View is
Accessed章节

Loading a view into memory from Apple Document

  • usernameTextField和passwordTextField一定要调用sendActionsForControlEvents法来通知UI已经更新。

[rootView.usernameTextField sendActionsForControlEvents:UIControlEventEditingChanged];
[rootView.passwordTextField sendActionsForControlEvents:UIControlEventEditingChanged];

一律开始时,我并从未调用sendActionsForControlEvents计导致loginViewModel.usernameloginViewModel.password特性并无更新。当时自家起来盘算,是未是还欲任何标准化尚会接触它创新也?由于自家使用UITextFieldrac_textSignal属性,于是我虽翻开其的源代码:

- (RACSignal *)rac_textSignal {
   @weakify(self);
   return [[[[[RACSignal
       defer:^{
           @strongify(self);
           return [RACSignal return:self];
       }]
       concat:[self rac_signalForControlEvents:UIControlEventEditingChanged |  UIControlEventEditingDidBegin]]
       map:^(UITextField *x) {
           return x.text;
       }]
       takeUntil:self.rac_willDeallocSignal]
       setNameWithFormat:@"%@ -rac_textSignal", self.rac_description];
}

从今源代码可以知晓,只有触及UIControlEventEditingChangedUIControlEventEditingDidBegin事件时才能够创建RACSignal对象。

ReactiveCocoa核心类设计

关于RAC核心类设计,官方文档有详尽的诠释:Framework
Overview

事务逻辑测试

鉴于此处要证实用户称以及密码,复用性高,我不以拍卖逻辑在viewModel中,而是定义一个DataValidation来处理。这里的用户名是邮箱格式,而密码要求长度超过等于6即可,方法如下:

@interface DataValidation : NSObject

+ (BOOL)isValidEmail:(NSString *)data;
+ (BOOL)isValidPassword:(NSString *)password;

@end

测试用例设计如下:

数据说明 Test Case.png

接下来下kiwi编写测试如下:

SPEC_BEGIN(DataValidationSpec)

describe(@"DataValidation", ^{
    context(@"when email is samlau@163.com", ^{
        it(@"should return YES", ^{
            BOOL result = [DataValidation isValidEmail:@"samlau@163.com"];
            [[theValue(result) should] beYes];
        });
    });

    context(@"when email is samlau163.com", ^{
        it(@"should return YES", ^{
            BOOL result = [DataValidation isValidEmail:@"samlau163.com"];
            [[theValue(result) should] beNo];
        });
    });

    ......省略两个测试用例
});

Sequence和Signal基本操作

询问完个RAC核心类设计下,要学会对Sequence和Signal基本操作,比如:用signal执行side
effects,转换streams, 合并stream和统一signal。详情请查阅官方文档:Basic
Operators

ViewModel层测试

面前早已完结了数据绑定和数量校验逻辑,接下去想使用谁类处理用户称以及密码是否中才能够点击和点击按钮后,如何调用网络层在来配合配用户名和密码,RAC提供一个RACCommand类。LoginViewModel概念一个特性loginCommand,并在兑现公文中使用Lazy Initialization初始化:

- (RACCommand *)loginCommand
{
    if (!_loginCommand) {
        _loginCommand = [[RACCommand alloc] initWithEnabled:[self isValidUsernameAndPasswordSignal] signalBlock:^RACSignal *(id input) {

            return [LoginClient loginWithUsername:self.username password:self.password];
        }];
    }
    return _loginCommand;
}

方有一个重要方法isValidUsernameAndPasswordSignal来监听与验证用户称及密码:

- (RACSignal *)isValidUsernameAndPasswordSignal
{
    return [RACSignal combineLatest:@[RACObserve(self, username), RACObserve(self, password)] reduce:^(NSString *username, NSString *password) {
         return @([DataValidation isValidEmail:username] && [DataValidation isValidPassword:password]);
    }];
}

是因为地方的方法isValidUsernameAndPasswordSignal早已监听LoginViewModel的username和password,当username和password其中一个改观时,DataValidation恍如都见面调用isValidEmailisValidPassword来数说明,并将结果包裹成RACSignal对象回来。

测试用例筹如下:

View Model Test Case

然后运kiwi编写测试如下:

describe(@"LoginViewModel", ^{
    __block LoginViewModel* viewModel = nil;

    beforeEach(^{
        viewModel = [LoginViewModel new];
    });

    afterEach(^{
        viewModel = nil;
    });

    context(@"when username is samlau@163.com and password is freedom", ^{
        __block BOOL result = NO;

        it(@"should return signal that value is YES", ^{
            viewModel.username = @"samlau@163.com";
            viewModel.password = @"freedom";

            [[viewModel isValidUsernameAndPasswordSignal] subscribeNext:^(id x) {
                result = [x boolValue];
            }];

            [[theValue(result) should] beYes];
        });
    });

    ......省略两个测试用例
});

如上测试用例很简短,设置viewModel的username和password,然后调用isValidUsernameAndPasswordSignal返回RACSignal对象,使用subscribeNext赢得其的价值,最后证实。

MVVM架构

MVVM high level.png

在MVVM架构中,通常还将view和view
controller看做一个整体。相对于事先MVC架构中view
controller执行很多以view和model之间数据映射和互相的劳作,现在拿其交给view
model去做。
至于选择哪种机制来更新view
model或view是没有强制的,但普通我们还挑ReactiveCocoa。ReactiveCocoa会监听model的变动然后以这些反映射到view
model的特性被,并且可实施有事情逻辑。
推个例子来说,有一个model包含一个dateAdded的特性,我眷恋监听它的变更然后更新view
model的dateAdded属性。但model的dateAdded属性的数据类型是NSDate,而view
model的数据类型是NSString,所以在view
model的init方法被展开数据绑定,但用多少类型转换。示例代码如下:

RAC(self,dateAdded) = [RACObserve(self.model,dateAdded) map:^(NSDate*date){ 
    return [[ViewModel dateFormatter] stringFromDate:date];
}];

ViewModel调用dateFormatter进行数量易,且方法dateFormatter可以复用到其它地方。然后view
controller监听view model的dateAdded属性且绑定到label的text属性。

RAC(self.label,text) = RACObserve(self.viewModel,dateAdded);

今日我们抽象出日期变到字符串的逻辑到view
model,使得代码可以测试复用,并且帮view controller瘦身

网络层测试

末尾处理点击登录按钮访问服务器来证实用户称与密码。我定义一个LoginClient仿佛来处理:

@interface LoginClient : NSObject

+ (RACSignal *)loginWithUsername:(NSString *)username password:(NSString *)password;

@end

不过设输入username和password两独参数,就可知返是否说明成功的结果被打包于RACSignal对象中。

由此地我是下moco效仿服务,所以才计划一个打响的测试用例:

Network Test Case.png

然后用kiwi编写测试如下:

describe(@"LoginClient", ^{
    context(@"when username is samlau@163.com and password is samlau", ^{
        __block BOOL success = NO;
        __block NSError *error = nil;

        it(@"should login successfully", ^{
            RACTuple *tuple = [[LoginClient loginWithUsername:@"samlau@163.com" password:@"samlau"] asynchronousFirstOrDefault:nil success:&success error:&error];
            NSDictionary *result = tuple.first;

            [[theValue(success) should] beYes];
            [[error should] beNil];
            [[result[@"result"] should] equal:@"success"];
        });
    });
});

中间用RAC的一个重点艺术asynchronousFirstOrDefault来测试异步网络访问的。详情可参看Test
with
Reactivecocoa文章。

Kiwi

Kiwi是一个iOS行为使开发(Behavior Driven
Development)的堆栈。相比于Xcode提供单元测试的XCTest是于测试的角度思考问题,而Kiwi是自从行为的角度思考问题,测试用例都循三段式Given-When-Then的叙述,清晰地表达测试用例是测试什么样的对象要数据结构,在依据什么上下文或现象,然后做出什么响应。

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

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

咱十分容易因上下文将那领取为Given..When..Then的三段式自然语言

Given a Team, when be newly created, it should have a name, it should have 11 player

用Xcode自带的XCTest测试框架写了测试代码的情侣可能体会到,以上代码更加容易阅读和掌握。就算以后来新的开发者入或者修护代码时,不需要极度老之财力去读与清楚代码。具体哪利用Kiwi,请参考两篇稿子:

  • TDD的iOS开发初步与Kiwi使用入门
  • Kiwi 使用上阶 Mock, Stub,
    参数捕获和异步测试

抓取网络数据并展示情景

大红鹰葡京会 2

如图所示,输入正确的用户称以及密码后,跳反到一个食列表页面,它打服务端抓到手图片、价格跟曾售份数后为列表的不二法门示。

Designer News UI

在编辑Designer
News客户端代码之前,首先通过UI来打探整个App的大概。设计Designer News
UI的工具是Sketch,想获得Designer
News UI,请点击下载Designer New
UI。

Designer News Design.png

设以享有的页面还逐项说明如何编写,会比较耗时间,所以就以登陆页面来证明我是哪行事使得开发iOS,但我会用尽项目之代码上传来github。

网络层测试

首先考虑怎样规划和贯彻API,然后又考虑怎么测试。因为其要打服务端抓取数据,需要规划一个走访食物列表数据的类似FoodListClient,设计如下:

@interface FoodListClient : NSObject

+ (RACSignal *)fetchFoodList;

@end

FoodListClient兑现如下:

@implementation FoodListClient

+ (RACSignal *)fetchFoodList
{
    return [[[AFHTTPSessionManager manager] rac_GET:[URLHelper URLWithResourcePath:@"/v1/foodlist"] parameters:nil] replayLazily];
}

@end

fetchFoodList方式主要从服务端抓取数据后,返回一个JSON格式的数组。因此想测试是API,只需要使用RAC的asynchronousFirstOrDefault主意返回RACTuple目标,获取第一个价,测试返回数组不也空即可。使用kiwi编写测试如下:

describe(@"FoodListClient", ^{

    context(@"when fetch food list ", ^{
        __block BOOL successful = NO;
        __block NSError *error = nil;

        it(@"should receive data", ^{
            RACSignal *result = [FoodListClient fetchFoodList];
            RACTuple *tuple = [result asynchronousFirstOrDefault:nil success:&successful error:&error];
            NSArray *foodList = tuple.first;

            [[theValue(successful) should] beYes];
            [[error should] beNil];
            [[foodList shouldNot] beEmpty];
        });
    });
});

登陆界面

由于这个项目简单以就生一个人开(基本上丁支付来说,采用Storyboard不易于代码合并),加上Storyboard可以可视化的添加UI组件和Auto
Layout的格,并且可以而且预览多单不同分辨率iPhone的功力,极大地提高开发界面效率。

Login.png

Model层测试

抓取了数据后,它的数量格式一般还是JSON格式,需要转接为Model方便访问与改,通常自己还利用Mantle来促成。我定义一个FoodModel类:

@interface FoodModel : MTLModel <MTLJSONSerializing>

/*
 @brief 食物图片URL
 */
@property (copy, nonatomic) NSString *foodImageURL;
/*
 @brief 食物价格
 */
@property (copy, nonatomic) NSString *foodPrice;
/*
 @brief 销量
 */
@property (copy, nonatomic) NSString *saleNumber;

@end

这就是说如何测试其是否转会成为?首先根据上一个网络层测试获取返回JSON格式的食品列表数据,然后调用MTLJSONAdapter类的modelsOfClass: fromJSONArray: error:方来转化成FoodModel的数组。接下来断言数组不克为空数组的首先单因素是FoodModel

用kiwi编写测试如下:

describe(@"FoodModel", ^{

    context(@"when JSON data convert to FoodModel", ^{
        __block BOOL successful = NO;
        __block NSError *error = nil;

        it(@"should return FoodModel array", ^{
            // get data from network
            RACSignal *result = [FoodListClient fetchFoodList];
            RACTuple *tuple = [result asynchronousFirstOrDefault:nil success:&successful error:&error];
            NSArray *foodList = tuple.first;

            // assert that foodList can't be empty
            [[theValue(successful) should] beYes];
            [[error should] beNil];
            [[foodList shouldNot] beEmpty];

            // assert that return FoolModel array
            NSArray *foodModelList = [MTLJSONAdapter modelsOfClass:[FoodModel class] fromJSONArray:foodList error:nil];
            [[foodModelList shouldNot] beEmpty];
            [[foodModelList[0] should] beKindOfClass:[FoodModel class]];
        });
    });
});

登陆交互

登陆界面有Email输入框和密码输入框,当用户选中其他一个输入框时,左边对应之图标变成蓝色,同时会发出pop动画表示用户准备而输入内容。
当用户并未输入有效的Email或密码格式时,用户是休可知点击登陆按钮,只有当用户输入有效的邮件和密码格式时,才会点击登陆按钮。

Login.gif

咱得利用RAC经监听Text
Field的UITextFieldTextDidBeginEditingNotificationUITextFieldTextDidEndEditingNotification的通来拍卖用户选中Email输入框和密码输入框时改变图标与出示的卡通。

#pragma mark - Text Field notification
- (void)textFieldStartEndEditing
{
    // Respond to when email text start and end editing
    [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidBeginEditingNotification object:self.emailTextField] subscribeNext:^(id x) {
        [self.emailImageView animate];
        self.emailImageView.image = [UIImage imageNamed:@"icon-mail-active"];
        self.emailTextField.background = [UIImage imageNamed:@"input-outline-active"];
    }];

    [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidEndEditingNotification object:self.emailTextField] subscribeNext:^(id x) {
        self.emailTextField.background = [UIImage imageNamed:@"input-outline"];
        self.emailImageView.image = [UIImage imageNamed:@"icon-mail"];
    }];

    // Respond to when password text start and end editing
    [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidBeginEditingNotification object:self.passwordTextField] subscribeNext:^(id x) {
        [self.passwordImageView animate];
        self.passwordTextField.background = [UIImage imageNamed:@"input-outline-active"];
        self.passwordImageView.image = [UIImage imageNamed:@"icon-password-active"];
    }];

    [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UITextFieldTextDidEndEditingNotification object:self.passwordTextField] subscribeNext:^(id x) {
        self.passwordTextField.background = [UIImage imageNamed:@"input-outline"];
        self.passwordImageView.image = [UIImage imageNamed:@"icon-password"];
    }];
}

当点击登陆按钮后,客户端向服务端发送验证请求,服务端验证了账户与密码后,用户就是可以成功登陆。所以,接下要了解RESTful
API的基本概念和Designer News提供的RESTful API。

ViewModel抓到手多少

完成抓取网络数据及转化JSON数据吧Model后,我用FoodViewModel抓取网络数据和完成数映射,设计与落实如下:

@interface FoodViewModel : RVMViewModel

/*
 @brief FoodModel列表
 */
@property (strong, nonatomic, readonly) NSArray *foodModelList;

@end

@implementation FoodViewModel

- (instancetype)init
{
    self = [super init];

    if (!self) {
        return nil;
    }

    RAC(self, foodModelList) = [[FoodListClient fetchFoodList] map:^id(RACTuple * tuple) {
        return [MTLJSONAdapter modelsOfClass:[FoodModel class] fromJSONArray:tuple.first error:nil];
    }];

    return self;
}

@end

Designer News API

Controller加载数据

最后FoodListViewController荷构建view hierarchy和加载数据:

#pragma mark - Lifecycle
- (void)viewDidLoad
{
    [super viewDidLoad];
    // setup title name and background color
    self.title = @"食物列表";
    self.view.backgroundColor = [UIColor whiteColor];
    // build view hierarchy
    [self buildViewHierarchy];
    // when finish fetching data and reload table view
    [RACObserve(self.foodViewModel, foodModelList) subscribeNext:^(NSArray* items) {
        self.foodListDataSource.items = items;
        [self.tableView reloadData];
    }];
}

RESTful API基本概念和筹划

REST全是Representational State
Transfer,翻译过来就是展现层状态转化。要想真正掌握它们的含义,从几单重点字入手:Resource,
Representation, State Transfer

  • ##### Resource(资源)

资源就是网络直达之实业,它好是文、图片、声音、视频或平等栽服务。但网络有这样多资源,该如何标识它们为?你得就此URL(统一资源定位符)来唯一标识及一定它们。只要获得资源对应之URL,你虽足以拜它们。

  • ##### Representation(表现层)

资源是同样栽信息实体,它发出多意味着法。比如,文本可以用.txt格式表示,也堪用xml、json或html格式表示。

  • ##### State Transfer(状态转换)

客户端访问服务端,服务端处理了后回来客户端,在是历程遭到,一般都见面挑起数据状态的更改或转换。
客户端操作服务端,都是透过HTTP协议,而在是HTTP协议中,有几只动词:GET,
POST, DELETEUPDATE

  • GET表示收获资源
  • POST表示新增资源
  • DELETE代表去资源
  • UPDATE表示更新资源

知情RESTful核心概念后,我们来概括了解RESTful API设计以便可以看懂Designer
News提供API。就以Designer News获取Stories对许URL的一个例来证实:
客户端请求
GET https://api-news.layervault.com/api/v1/stories?client_id=91a5fed537b58c60f36be1sdf71ed1320e9e4af2bda4366f7dn3d79e63835278

服务端返回结果(部分结出)

{
  "stories": [
    {
      "id": 46826,
      "title": "A Year of DuckDuckGo",
      "comment": "",
      "comment_html": null,
      "comment_count": 4,
      "vote_count": 17,
      "created_at": "2015-03-28T14:05:38Z",
      "pinned_at": null,
      "url": "https://news.layervault.com/click/stories/46826",
      "site_url": "https://api-news.layervault.com/stories/46826-a-year-of-duckduckgo",
      "user_id": 3334,
      "user_display_name": "Thomas W.",
      "user_portrait_url": "https://designer-news.s3.amazonaws.com/rendered_portraits/3334/original/portrait-2014-09-16_13_25_43__0000-333420140916-9599-7pse94.png?AWSAccessKeyId=AKIAI4OKHYH7JRMFZMUA&Expires=1459149709&Signature=%2FqqLAgqpOet6fckn4TD7vnJQbGw%3D",
      "hostname": "designwithtom.com",
      "user_url": "http://news.layervault.com/u/3334/thomas-wood",
      "badge": null,
      "user_job": "Online Designer at IDG UK",
      "sponsored": false,
      "comments": [
        {
          "id": 142530,
          "body": "Had no idea it had those customization settings — finally making the switch.",
          "body_html": "<p>Had no idea it had those customization settings — finally making the switch.</p>\\n",
          "created_at": "2015-03-28T18:41:37Z",
          "depth": 0,
          "vote_count": 0,
          "url": "https://api-news.layervault.com/comments/142530",
          "user_url": "http://news.layervault.com/u/3826/matt-soria",
          "user_id": 3826,
          "user_display_name": "Matt S.",
          "user_portrait_url": "https://designer-news.s3.amazonaws.com/rendered_portraits/3826/original/portrait-2014-04-12_11_08_21__0000-382620140412-5896-1udai4f.png?AWSAccessKeyId=AKIAI4OKHYH7JRMFZMUA&Expires=1459125745&Signature=%2BDdWMtto3Q10dd677sUOjfvQO3g%3D",
          "user_job": "Web Dood @ mattsoria.com",
          "comments": []
        },
  • 协议(protocol)
    用户以及API通信采用HTTPs协议
  • 域名(domain name)
    应尽量部署至专用域名下https://api-news.layervault.com/,但奇迹见面尤其扩充为https://api-news.layervault.com/api
  • 版本(version)
    应该将API版本号v1放入URL
  • 路径(Endpoint)
    路径https://api-news.layervault.com/api/v1/stories代表API具体网址,代表网同样栽资源,所以未可知有动词,只有应用名词来表示。
  • HTTP动词
    动词GET,表示于服务端获取Stories资源
  • 过滤信息(Filtering)
    ?client_id=91a5fed537b58c60f36be1sdf71ed1320e9e4af2bda4366f7dn3d79e63835278指定client_id的Stories资源
  • 状态码(Status Codes)
    服务器向客户端返回表示成功或破产的状态码,状态码列表请参考Status
    Code
    Definitions
  • 错误处理(Error handling)
    服务端处理用户要失败后,一般都回error字段来表示错误信息

{
    error: "Invalid client id"
}

总结

修单元测试是程序员的一模一样宗基本技能,如果能统筹好之测试用例并编写测试证明结果,不仅保证代码的质地,而且好下重构加相同层保护层。一旦修改了代码之后,如果运行单元测试,并不曾经的话,说明你在重构过程中引入新的bug。如果经过了单元测试,说明并没有引入新的bug。

Designer News提供API

Designer News API
Reference供基于HTTP合计遵循RESTful设计的API,并且同意应用程序通过oAuth
2授权协议来取得授权权限来拜访用户信息。

推而广之阅读

  • ReactiveCocoa
    Test with
    Reactivecocoa
  • Kiwi
    TDD的iOS开发初步与Kiwi使用入门
    Kiwi 使用上阶 Mock, Stub,
    参数捕获和异步测试

访问API工具

相似的话,在描写访问服务端代码之前,我都见面用Paw(下载地址)工具来测试API是否行得通;另一方面,用JSON文件保留服务端返回的多少,用于moco学服务端的服务。至于怎么用moco模拟服务端,后面会讲课,现在由此用户登录Designer
News
这事例介绍如何利用Paw来测试API。
咱们事先看看Designer News提供访问用户登录的API

Designer News Login API.png

依据上述提供的消息,API的门道是https://api-news.layervault.com/oauth/token,参数有grant_typeusernamepasswordclient_secret。其中usernamepassword在Designer
News挂号才会取,而client_idclient_secret消发送email到news@layervault.com报名。使用Paw发送请求与服务端返回结果如下:

New Send Request.png

Moco模拟服务端

Moco凡是一个得轻松搭建测试服务器的工具。

怎用效法服务端

作为一个移动开发人员,有时由服务端开发进度慢,空来一个iPhone应用可表达不发出图。幸好有矣Moco,只待配备一下央和归数据,很快便可以搭建一个法服务,无需等待服务端支付好才能连续出。当服务端完成后,修改访问地址即可。

有时候服务端API应该是啊法都还不曾掌握,由于来矣moco模拟服务,在支付过程中,可以不断调整API设计,搞懂真正好想使的API是什么则的。就这么,在服务端代码还没当真动手前,已经提供平等客真正满足自己要的API文档,剩下的就是付给劳务端照着API去落实就实行了。

再有平等种植状况便是,服务端已经写好了,剩下客户端还从来不成功。由于moco是本地服务,访问速度比较快,所以经过行使moco来模拟服务端,这样不光可以提高客户端的访问速度,还增强网络层测试代码访问速度的安定,Designer
News就是如此情况。

什么样以Moco模拟服务

安装

一旦你是动Mac或Linux,可以品味一下步骤:

  1. 规定你安装JDK 6以上
  2. 下载脚本
  3. 拿它们座落你的$PATH路径
  4. 安装它可尽(chmod 755 ~/bin/moco)

当今而可以运行一下命令测试安装是否成

  1. 编写配置文件foo.json,内容如下:

[
      {
        "response" :
          {
            "text" : "Hello, Moco"
          }
      }
]
  1. 运行Moco HTTP服务器
    moco start -p 12306 -c foo.json
  2. 开辟浏览器访问http://localhost:12306,你扭曲看见”Hello, Moco”
布服务

是因为发生时分服务端返回的数目较多,所以将服务端响应的数码独立在一个JSON文件被。以登陆为例,将数据存放于login_response.json

{
    "access_token": "4422ea7f05750e93a101cb77ff76dffd3d65d46ebf6ed5b94d211e5d9b3b80bc",
    "token_type": "bearer",
    "scope": "user",
    "created_at": 1428040414
}

若用呼吁uri路径,方法(method)和参数(queries)等布置在login_conf.json文件中

[
  {
    "request" :
      {
        "uri" : "/oauth/token",
        "method" : "post",
        "queries" : 
          {
            "grant_type" : "password",
            "username" : "liuyaozhu13hao@163.com",
            "password" : "freedom13",
            "client_secret" : "53e3822c49287190768e009a8f8e55d09041c5bf26d0ef982693f215c72d87da",
            "client_id" : "750ab22aac78be1c6d4bbe584f0e3477064f646720f327c5464bc127100a1a6d"
          }
      },
    "response" :
      {
        "file" : "./Login/login_response.json"
      }
  }
]

不亮堂发生没产生在意到端uri路径不是咸路线http://localhost:12306/oauth/token,因为协议默认是http,而且一般运行于本机localhost,所以于启动拟服务经常止需要点名端口12306纵使实施。想进一步详细摸底怎么布置,请查阅官网的HTTP(s)
APIs
还有一个需配备地方便是,由于实在开发被得不止一个客户端请求,所以还待一个配置文件settings.json来含有很有请。

[
    {
        "include" : "./Story/stories_conf.json"
    },
    {
        "include" : "./Login/login_conf.json"
    },
    {
        "include" : "./Story/story_upvote_conf.json"
    }
]
起步服务

以路径跳反到DesignerNewsForObjc/DesignerNewsForObjcTests/JSON目,找到settings.json文件,使用命令执行来启动服务:
moco start -p 12306 -g settings.json

行使Paw验证是否安排成功

Send request to Local Server.png

作为使得开发(BDD)

怎么要BDD

无知道诸位在编制测试的时段,有没有发出思想过一个问题:我应该测试什么?要报这问题大红鹰葡京会并无是那粗略,在没取得答案之前,你还是连续遵循卿的想法编写测试。
-(void)testValidateEmail;
比如这么的测试,存在一个从问题。它不会见报您应当会时有发生啊,也不见面预期实际会产生啊。还有,当她发出误时,不见面提醒您当何出误,错误的由来是什么,因此你要深刻代码才会知晓失败的案由。这样就算得大量附加和无必要之认知负荷。
这BDD出现了,帮助开发者确定应测试什么,它提供DSL(Domain-specific
language,
域特定语言),测试用例都按照三截式Given-When-Then的叙说,清晰地发表测试用例是测试什么样的目标或数据结构,在因什么上下文或气象,然后做出什么响应。
故此,我们当关注行为,而不是测试。那行为具体是啊?当你设计app里面的中间目标时,它的接口定义方法及其依赖关系,这些点子以及负关系决定了您的靶子如何跟外对象交互,以及它的机能是啊,定义你的目标的行为

BDD过程

行让开发大概三单步骤:

  1. 选取最为要害的表现,并编辑行为之测试文件。此时,由于测试对象的类还从未修,所以编译失败。创建测试对象的切近并编辑类的野鸡实现,让编译通过。
  2. 实现深受测试类的表现,让测试通过。
  3. 倘发现代码中产生重代码,重构让测试类来祛除重

如临时无掌握中步骤细节,没有关系,继续为下看,后面来例子介绍来协助您明白三单步骤的义。

登陆验证

网访问层

DesignerNewsURL

DesignerNewsURL恍如包装网络访问URL

#import <Foundation/Foundation.h>

extern NSString* const baseURL;
extern NSString* const clientID;
extern NSString* const clientSecret;

@interface DesignerNewsURL : NSObject

+ (NSString*)loginURLString;
+ (NSString*)stroiesURLString;
+ (NSString*)storyIdURLStringWithId:(NSInteger)storyId;
+ (NSString*)storyUpvoteWithId:(NSInteger)storyId;
+ (NSString*)storyReplyWithId:(NSInteger)storyId;
+ (NSString*)commentUpvoteWithId:(NSInteger)commentId;
+ (NSString*)commentReplyWithId:(NSInteger)commentId;

@end

这边尚时有发生只技术就是是在DesignerNewsURL.m兑现公文来个标准化编译,判断是当测试环境还是产品环境来控制baseURL的价值,可以老有利于测试环境与制品环境相切换。

#ifndef TEST
NSString* const baseURL = @"https://api-news.layervault.com";
#else
NSString* const baseURL = @"http://localhost:12306";
#endif

NSString* const clientID = @"750ab22aac78be1c6d4bbe584f0e3477064f646720f327c5464bc127100a1a6d";
NSString* const clientSecret = @"53e3822c49287190768e009a8f8e55d09041c5bf26d0ef982693f215c72d87da";
作为令开发LoginClient

当编排代码之前,我们该先想想如何统筹LoginClient仿佛。首先根据Single
responsibility
principle(责任单一原则),LoginClient要负责用户登录的网络访问。需要提供一个接口,只要加用户称(username)和密码(password),用户就可知登录,由于自家是采取RAC来拍卖回来结果,所以这接口返回RACSignal对象。

  • 创一个LoginClientkiwi文件,编写对应行为。

Create LoginClient 1.png

Create LoginClient 2.png

SPEC_BEGIN(LoginClientSpec)

describe(@"LoginClient", ^{

    context(@"when user input correct username and password", ^{
      __block RACSignal *loginSignal;

      beforeEach(^{
          NSString *username = @"liuyaozhu13hao@163.com";
          NSString *password = @"freedom13";
          loginSignal = [LoginClient loginWithUsername:username password:password];
      });

      it(@"should return login signal that can't be nil", ^{
          [[loginSignal shouldNot] beNil];
      });

      it(@"should login successfully", ^{
          __block NSString *accessToken = nil;

          [loginSignal subscribeNext:^(NSString *x) {
              accessToken = x;
              NSLog(@"accessToken = %@", accessToken);
          }error:^(NSError *error) {
              [[accessToken shouldNot] beNil];
          } completed:^{
              [[accessToken shouldNot] beNil];
          } ];
      });

    });
});

SPEC_END

因三段式Given-When-Then叙,上面代码我们得以知道啊:在叫定LoginClient对象,当用户输入是的用户称以及密码时,应该登录成功。
此刻,由于还从未创建LoginClient恍如,所以会见无经编译,创建LoginClient看似,并编辑它的伪实现,让LoginClientSpec.m透过编译。

LoginClient.h.png

LoginClient.m.png

运行测试,测试失败。

LoginClient Failed.png

  • 实现LoginClient,通过该测试

LoginClient.m .png

LoginClient Pass Test.png

  • 鉴于无冗余代码,无需重构

Model层

由这次登陆请求服务端返回数据比较简单,只是获得access_token配段数据,所以无需要model来照和储存数据。不过在取多只Stories时,就会见下到model来拍卖。

Controller与ViewModel层

controller凡是拍卖用户交互的输入,通常自己都见面用拍卖用户交互的逻辑、数据绑定和多少校验都提交ViewModel来精简controller代码,同时最要命程度地复用业务逻辑的代码。
咱们事先想起用户登陆时的步调:1.
用户优先输入email和密码,只有email和密码可格式要求时才会点击按钮。2.
用户成功登陆后,跳反到故事列表主页。
咱俩先分析一下哪实现步骤1,
想只要本着email和密码进行验证,必须要监听它们简单只价的变通,所以用针对emailTextFieldpasswordTextField使用RAC展开多少绑定。

创建LoginViewControllerSpeckiwi文件,测试绑定行为代码如下:

SPEC_BEGIN(LoginViewControllerSpec)

describe(@"LoginViewController", ^{
    __block LoginViewController *controller;

    beforeEach(^{
        controller = [UIViewController loadViewControllerWithIdentifierForMainStoryboard:@"LoginViewController"];
        [controller view];
    });

    afterEach(^{
        controller = nil;
    });

    describe(@"Email Text Field", ^{
        context(@"when touch text field", ^{
            it(@"should not be nil", ^{
                [[controller.emailTextField shouldNot] beNil];
            });
        });

        context(@"when text field's text is hello", ^{
            it(@"shoud euqal view model's email property", ^{
                controller.emailTextField.text = @"hello";
                [controller.emailTextField sendActionsForControlEvents:UIControlEventEditingChanged];
                [[controller.viewModel.email should] equal:@"hello"];
            });
        });
    });

    describe(@"Password Text Field", ^{
        context(@"when touch text field", ^{
            it(@"should not be nil", ^{
                [[controller.passwordTextField shouldNot] beNil];
            });
        });

        context(@"when text field' text is hello", ^{
            it(@"should equal view model's password property", ^{
                controller.passwordTextField.text = @"hello";
                [controller.passwordTextField sendActionsForControlEvents:UIControlEventEditingChanged];

                [[controller.viewModel.password should] equal:@"hello"];
            });
        });
    });
});

SPEC_END

此间有点儿只第一点,一个凡自从Storyboard中加载controller,否则不能够获得emailTextField和password,如果运用手写UI代码就非欲了。另一个就算是emailTextField或passwordTextField必须调用sendActionsForControlEvents:UIControlEventEditingChanged道,才能够触发textField的text属性改变。

编译失败后,在LoginViewController.m编写- (void)bindViewModel措施通过测试

RAC(self.viewModel, email) = self.emailTextField.rac_textSignal;
RAC(self.viewModel, password) = self.passwordTextField.rac_textSignal;

兑现了数据绑定行为后,接下去要数校验,交给LoginViewModel来处理。创建LoginViewModelSpec.m文件,提供emailpassword属性给LoginViewModel,返回验证结果的RACSignal,测试证明行为代码如下:

SPEC_BEGIN(LoginViewModelSpec)

describe(@"LoginViewModel", ^{
    // Initialize
    __block LoginViewModel *viewModel;

    beforeEach(^{
        viewModel = [[LoginViewModel alloc] init];
    });

    afterEach(^{
        viewModel = nil;
    });

    context(@"when email and password is valid", ^{
        it(@"should get valid signal", ^{
            viewModel.email = @"liuyaozhu13hao@163.com";
            viewModel.password = @"123456";

            __block BOOL result;

            [[viewModel checkEmailPasswordSignal] subscribeNext:^(id x) {
                result = [x boolValue];
            } completed:^{
                [[theValue(result) should] beYes];
            }];
        });
    });

    context(@"when email is valid, but password is invalid", ^{
        it(@"should get invalid signal", ^{
            viewModel.email = @"liuyaozhu13hao@163.com";
            viewModel.password = @"1";

            __block BOOL result;

            [[viewModel checkEmailPasswordSignal] subscribeNext:^(id x) {
                result = [x boolValue];
            } completed:^{
                [[theValue(result) shouldNot] beYes];
            }];
        });
    });

    context(@"when password is valid, but email is invalid", ^{
        it(@"should get invalid signal", ^{
            viewModel.email = @"liuyaozhu";
            viewModel.password = @"123456";

            __block BOOL result;
            [[viewModel checkEmailPasswordSignal] subscribeNext:^(id x) {
                result = [x boolValue];
            } completed:^{
                [[theValue(result) shouldNot] beYes];
            }];
        });
    });
});

SPEC_END

编译失败后(已经创办LoginViewModel类),添加- (RACSignal*)checkEmailPasswordSignal连实现认证数据,通过测试

- (RACSignal*)checkEmailPasswordSignal
{
    RACSignal* emailSignal = RACObserve(self, email);
    RACSignal* passwordSignal = RACObserve(self, password);

    return [RACSignal combineLatest:@[ emailSignal, passwordSignal ] reduce:^(NSString* email, NSString* password) {
        BOOL result = [email isValidEmail] && [password isValidPassword];

        return @(result);
    }];
}

末了用以LoginViewModel创建属性为loginButtonCommandRACCommand来处理点击登陆按钮的相互。在LoginViewControllerSpec.m测试loginButton.rac_command非克吧空

describe(@"Login Button", ^{
      context(@"when load view", ^{
            it(@"should be not nil", ^{
                [[controller.loginButton shouldNot] beNil];
            });

            it(@"should have rac command that not be nil", ^{
                [[controller.loginButton.rac_command shouldNot] beNil];
            });
      });
 });

测试失败,在LoginViewController.m编写- (void)bindViewModel艺术以下代码有

self.loginButton.rac_command = self.viewModel.loginButtonCommand;

LoginViewModel.m推迟初始化loginButtonCommand属性

#pragma mark - Lazy initialization
- (RACCommand*)loginButtonCommand
{
    if (!_loginButtonCommand) {
        _loginButtonCommand = [[RACCommand alloc] initWithEnabled:[self checkEmailPasswordSignal] signalBlock:^RACSignal * (id input) {
            self.active = YES;

            return [[LoginClient loginWithUsername:self.email password:self.password] doNext:^(NSString *token) {
                self.active = NO;
                // Save the token
                [LocalStore saveToken:token];
                // Dismiss view controller and fetch data, reload
                self.dismissBlock();
            }];
        }];
    }

    return _loginButtonCommand;
}

由此测试,完成登陆基本流程,至于登陆成功后怎样回到故事列表页面,这里不详细介绍,各位可经翻阅工程代码尽管好赢得答案。

总结

近日一段时间都更拘留关于敏捷开发之图书(用户故事以及敏捷方法,硝烟中的Scrum和XP,
剖析极限编程),对便捷开发很感兴趣,但意识很少公司或博客介绍如何履敏捷开发iOS,所以于网上征集一些素材,发现发许多优秀的实行(测试驱动开发,重构,持续集成测试,增量设计,增量计划)值得去上学,通过自己对便捷开发中各种实践的了解来再写这Designer
News,这个Designer
News功能还并未满完成,希望各位看了马上篇稿子尝试以这样方式来完成全部app。如果我有些意见还是执行理解有无意,请各位多多指。

壮大阅读

  • ReactiveCocoa
    ReactiveCocoa –
    iOS开发的新框架
    ReactiveCocoa2实战
    ReactiveCocoa Essentials: Understanding and Using
    RACCommand
    Test with
    Reactivecocoa
  • Kiwi
    TDD的iOS开发初步与Kiwi使用入门
    Kiwi 使用上阶 Mock, Stub,
    参数捕获和异步测试
  • RESTful API
    理解RESTful架构
    RESTful API
    设计指南
    理解OAuth
    2.0
    SSL/TLS协议运行机制的概述
  • Moco
    Moco能集成测试,还能够移动开;能前端开发,还能模拟Web服务器!
  • 测试
    表现让开发
    XCTest
    测试实战
    依靠注入
    糟糕的测试
    换成测试: Mock, Stub
    和其他

相关文章

admin

网站地图xml地图