美文网首页
行为驱动开发

行为驱动开发

作者: 纵横而乐 | 来源:发表于2015-08-09 17:34 被阅读405次

    翻译自objc.io behavior-driven-development

    开始对自己的代码进行自动化测试并不是一件轻松的事情,如果没有人帮你做一些关于测试的大致梳理,可能你面对的情况会是这样:我一次又一次地听说了TDD,自己也要行动起来了。于是坐下来开始写测试代码,对功能进行一些测试,但在这开始之前,需要搞清楚一个基本的问题:应该测试什么。

    比如你写了这样一句代码 testDownloadData, 但其实对于它的执行会发生什么并没有界定,也没有说明什么样的结果是希望得到的,也就是说这样的测试并没有阐明测试的要求。

    而且,当测试失败的时候,需要深入到源代码中以探求失败的原因。而在测试做得好的理想情况下,这是不需要的。

    也正因为如此,行为驱动开发应运而生,它旨在解决上述问题,即应该测试什么。此外,它还提供了DSL以帮助开发者厘清他们需要测试的内容,并介绍了一份普遍的语言以帮助开发都理解测试的目的是什么。

    应该测试什么

    这个意味深远的问题答案其实挺简单的,但需要你对你的测试代码的感知进行适当的转变。因为BDD的第一个词就告诉我们应当关注行为而非测试,也就是说我们应当对行为开始测试。

    举个栗子吧,app中的某个对象,它定义了方法及依赖的接口,这些接口及依赖即是这个对象的契约,它定义了这个对象与app中其它部分交互的方式,以及其所拥有的功能,这些即是其行为。

    BDD DSL

    在惊叹BDD DSL带给我们的便捷之前,先来看一个它所呈现的基本测试代码:

    behavior驱动测试代码样例

    SpecBegin定义了名为CarSpec的测试类,SpecEnd结束这个类声明,describe参数中的block定义一组测试样例。context block的作用与describe类似,这种写法俗称语法糖。it是一个独立的测试样例。

    beforeEach是在同一代码嵌套层级中及以下层级中的block调用之前 被调用的block。可能你也注意到了,这个DSL中定义的几乎所有组件都包含两个部分:一个定义当前在测试什么的字符串,一个包含更多组件或者测试内容的block。

    其中这个字符串有两个重要的作用:首先在describe块中,这些字符串起到将所测试的部分功能区隔成一组一组的作用,这样就可以在 一层层嵌套的测试中不至于迷失了各描述所对应的功能部分,于是代码作者也可以根据上下文填写对象特定的描述信息或者依赖信息。这也正是describe:move to block所做的,我们分别为car的两种不同状态创建了两个context block以验证不同的结果。这个例子也演示了BDD DSL所要求的需要清晰地测试对象在不同情况下不同的行为。

    另外,这些字符串还会构成测试失败时的提醒语句。假设车未启动时那条测试失败,则收到的错误信息会是“Car move to when engine is not running should not move to given position”,这样的错误信息最小化了认知负担,让我们更好地理解了这个错误的内容。

    但并不是说只有BDD风格的语法可能给出这么清晰的测试语句和易读的测试信息,只是说BDD提供了更为方便的给出这样的测试和出错信息的功能及语法。如果想了解更多BDD语法,可以参考specta guide for writing specs

    BDD frameworks

    iOS/Mac开发者有很多可供选择的BDD frameworks:Cedar,Kiwi,Specta

    在语法方面,这些frameworks几乎是一样了,其不同之处在于它们的可配置性和集成的组件不同。

    Cedar集成的是matchersdoubles,虽然并不一定准确,但在这里我们将doubles当作mocks。除了这些功能之外,cedar还有另一项特性可供配置:聚焦测试。cedar可以通过在it,describe或者context block前面添加f以实现对一个特定测试项或者一个测试项组的单独测试。

    还有一个相反的配置功能:通过添加 x 将某项测试关闭。XCTest虽然也有类似的配置能力,但需要借助Scheme的帮助,即需要通过手动触发“run this test”,所以Cedar的配置更简单且更快。

    Cedar使用了一些hackery的技能以与XCTest整合,如果apple有一天更改了内部实现,这个整合可能就会被打破了。

    Kiwi也整合了matchers,mocks和stubs,与Cedar不同的是,Kiwi与XCTest紧密地整合在一起,但不及Cedar那样配置方便快捷。

    涉及到测试工具的时候,specta提供了一个不同的方法,它并没有集成matchers,mocks,stubs,而是提供了一个单独的包含了很多matchers的Expecta库。它还与XCTest紧密整合,并提供了类似Cedar的聚焦测试和x关闭测试项。

    可以看出上述测试框架均有自己的优势,选择使用哪个只能根据个人喜好了。不得不提到的是,已经有两个BDD框架支持Swift了:QuickSleipnir

    样例

    在开始样例之前,需要强调的是要写出良好的行为测试代码,需要对依赖的良好识别以及在接口中对这些依赖的暴露接口。

    大多数的测试可能都是根据测试对象的状态,断言特定交互是否发生,或者是否返回了特定的值,或者特定的值传递给另外一个对象。暴露依赖的接口可以使mock值和状态变得更简单,更进一步,其可以更方便地断言特定动作是否发生或者特定值是否被计算出来。

    需要注意的是,不要把所有对象的依赖和属性放在接口中,虽然测试的时候你会很想这么做。因为这会降低对象的可读性以及你的对象的清晰意图,然而你的对象的接口也需要设计明确以体现出其设计目的。

    Message Formatter

    我们从一个格式化给定事件对象的文本消息的例子入手

    Message Formatter Event protocol

    我们的目标是测试EventDescriptionFormatter是否返回类似“My Event starts at Aug 21, 2014, 12:00 AM and ends at Aug 21, 2014, 1:00 AM”的格式化描述。

    需要注意的是,上述例子使用的是mocking框架,如果你从未使用过可参考

    我们先通过mocking这个组件中唯一的依赖,即date formatter。我们会使用创建的mock为开始和结束时间返回固定的字符串,并检查返回的字符串是否是使用我们刚刚mock过的值创建的。

    mock样例

    但我们其实只测试了EventDescriptionFormatter是否使用了它的NSDateFormatter来格式化日期,但并未测试格式化的风格。所以,为了组成一个完整的测试组件,需要添加两个测试以测试格式化风格。

    然而上述例子并没有真正测试EventDescriptionFormatter的行为,而只是通过mocking内部实现的属性NSDateFormatter以进行测试。但实际上,我们并不关心是否存在一个date formatter。从接口的角度来看,我们其实只关注是否得到想要的字符串,即这才是我们关心的行为。

    我们可以通过不mocking NSDateFormatter轻易实现,而且我们并不关心它是否存在,所以可以将其移除:

    @interface EventDescriptionFormatter : NSObject

    - (NSString *)eventDescriptionFromEvent:(id)event;

    @end

    下一步即是重构测试,现在我们不用知道event formatter的内部实现,只需要聚焦在实际的行为上:

    可以看出,测试代码就已经很简单了。我们只是在一个简约的初始化block中准备了一个data model,然后调用了一个测试方法。通过更专注于行为的结果,而不是其具体的行为方式,简化了测试代码,同时又维持了我们的对象的函数测试。这也正是BDD的特点,只关注行为的结果,而不是实际的实现。

    Data Downloader

    在这个例子中,我们会构建一个简单的数据下载器。并专注于数据下载器的一个行为:请求并取消下载。先定义接口:

    CalendarDataDownloader

    当然还有网络层:

    @interface NetworkLayer : NSObject

    // Returns an identifier that can be used for canceling a request.

    - (id)makeRequest:(id)request completion:(void (^)(id, id, NSError *))completion;

    - (void)cancelRequestWithIdentifier:(id)identifier;

    @end

    接下来,我们会先检查实际的下载是否发生,mock的网络层已经创建并注入一个describe block中:

    describe(@"update calendar data", ^{

    beforeEach(^{

    [calendarDataDownloader updateCalendarData];

    });

    it(@"should make a download data request", ^{

    [verify(mockNetworkLayer) makeRequest:instanceOf([CalendarDataRequest class]) completion:anything()];

    });

    });

    这个部分十分简单,下一步是检查在调用cancel的时候是否取消了这个请求。

    请求的标识identifier是CalendarDataDownloader的一个私有属性,所以我们将需要将其暴露在我们的测试中

    @interface CalendarDataDownloader (Specs)

    @property(nonatomic, strong) id identifier;

    @end

    你可能会评测出,这些测试可能有点隐患。即使这些测试有效并检查了特定的行为,但还是暴露了CalendarDataDownloader的具体实现。我们的测试并没有需要知道CalendarDataDownloader如何持有其请求identifier。我们试试如何不暴露内部实现:

    我们通过stubbing  makeRequest:completion:方法,返回一个固定的identifier。在同一个describe block中,我们定义了一个cancel describe block,其会调用CalendarDataDownloader对象的cancel方法。然后会检查这个固定的字符串是否传给了我们mock过的网络层cancelRequestWithIdentifier:方法。

    注意到在这个时候,实际上不需要检查实际上是否发出了网络请求。我们仍然保留了测试是为了确保我们知道如果功能失效时会发生什么。

    这样,不用暴露CalendarDataDownloader实现细节就测试了其行为。并且只做了3个测试,而不是以前的4个。同时我们也利用了BDD DSL的嵌套功能以实现一连串的行为模拟。我们先模拟了下载,然后在同一个describe block中模拟了取消请求。

    Testing View Controllers

    ios开发者中普遍的态度是并不测试view controller,由于view controller通常代表着应用的核心部分,也是所有组件粘合在一起的地方,以及数据模型,应用逻辑与UI联系的地方。任何更改都可能引起极大的损伤。

    所以对view controller的测试十分重要,也不是一件容易的事。如下的上传照片和注册登录view controller的例子,可以演示可以怎样使用BDD来简化view controller的测试。

    Upload Photo View Controller

    在这个例子中,我们会创建一个简单的图片上传View Controller,包含一个发送按钮rightBarButtonItem,点按之后即通知上传组件。

    @interface PhotoUploadViewController : UIViewController

    @property(nonatomic, readonly) PhotoUploader *photoUploader;

    - (instancetype)initWithPhotoUploader:(PhotoUploader *)photoUploader;

    @end

    定义了一个外部依赖 photoUploader,实现很简单,不需要获取照片的逻辑,使用空的UIImage即可。

    相关文章

      网友评论

          本文标题:行为驱动开发

          本文链接:https://www.haomeiwen.com/subject/kogbqttx.html