2018正版葡京赌侠诗行事使得开发iOS

Designer News.png

前段时间在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。

ReactiveCocoa

ReactiveCocoa是一个用Objective-C编写,具有函数式和响应式特性的编程框架。大多数的开发者他们缓解问题的思想情势都是怎样做到任务,平常的做法就是编辑很多限令,然后修改重点数据结构的情况,这种编程范式叫做命令式编程(Imperative
Programming
)。与命令式编程不同的是函数式编程(Functional
Programming
),思考问题的情势是完结什么任务,咋样描述这一个职责。关于对函数式编程入门概念的敞亮,可以参见酷壳《函数式编程》这篇小说,深切浅出对函数式编程的考虑格局、特性和技能通过一些示范来讲学。

ReactiveCocoa解决哪些问题?

  • 目的之间状态与气象的倚重过多问题
    借用ReactiveCocoa中一个例证来证实:用户在签到界面时,有一个用户名输入框和密码输入框,还有一个记名按钮。登录交互要求如下:
  1. 当用户名和密码符合验证格式,并且以前还没登录时,登录按钮才能点击。
  2. 当点击登录成功登录后,设置已登录状态。

传统的做法代码如下:

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主旨类设计时,你就会询问它们中间的关系和什么采取。

ReactiveCocoa核心类设计

有关RAC主题类设计,官方文档有详尽的讲演:Framework
Overview

Sequence和Signal基本操作

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

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瘦身

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,请参见两篇作品:

Designer News UI

在编写Designer
News客户端代码从前,首先通过UI来询问任何App的大概。设计Designer News
UI的工具是Sketch,想获得Designer
News UI,请点击下载Designer New
UI

Designer News Design.png

假定将具有的页面都依次表明什么编写,会相比较耗时间,所以只拿登陆页面来证实自身是何许行事使得开发iOS,但我会将全体项目的代码上盛传github

登陆界面

出于那多少个项目简单并且只有一个人付出(两人支付以来,采纳Storyboard不易于代码合并),加上Storyboard可以可视化的添加UI组件和Auto
Layout的羁绊,并且可以同时预览六个不同分辨率Nokia的功能,极大地提高开发界面效率。

Login.png

登陆交互

登陆界面有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。

Designer News API

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"
}

Designer News提供API

Designer News API
Reference
提供按照HTTP情商服从RESTful设计的API,并且同意应用程序通过oAuth
2
授权协议来赢得授权权限来拜会用户音讯。

访问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。其中usernamepasswordDesigner
News
登记才能拿到,而client_idclient_secret亟待发送email到news@layervault.com提请。使用Paw发送请求和服务端再次回到结果如下:

New Send Request.png

Moco模拟服务端

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

何以需要效法服务端

用作一个平移开发人员,有时出于服务端开发进度慢,空有一个红米应用但表明不出效率。幸好有了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)

2018正版葡京赌侠诗,怎么需要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,否则无法博得emailText菲尔德(Field)和password,假若利用手写UI代码就不需要了。另一个就是emailText菲尔德(Field)(Field)或passwordTextField(Field)必须调用sendActionsForControlEvents:UIControlEventEditingChanged情势,才能触发text菲尔德(Field)(Field)的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。如果自己多少意见或举行精通有误,请各位多多指引。

壮大阅读

相关文章