【CodeTest】TDD,BDD及初步使用Quick

学习文章

TDD的必要性

以下引自王巍大神的博客:

测试驱动开发(Test Driven Development,以下简称TDD)是保证代码质量的不二法则,也是先进程序开发的共识。

测试驱动开发并不是一个很新鲜的概念了。软件开发工程师们(当然包括你我)最开始学习程序编写时,最喜欢干的事情就是编写一段代码,然后运行观察结果是否正确。如果不对就返回代码检查错误,或者是加入断点或者输出跟踪程序并找出错误,然后再次运行查看输出是否与预想一致。如果输出只是控制台的一个简单的数字或者字符那还好,但是如果输出必须在点击一系列按钮之后才能在屏幕上显示出来的东西呢?难道我们就只能一次一次地等待编译部署,启动程序然后操作UI,一直点到我们需要观察的地方么?这种行为无疑是对美好生命和绚丽青春的巨大浪费。于是有一些已经浪费了无数时间的资深工程师们突然发现,原来我们可以在代码中构建出一个类似的场景,然后在代码中调用我们之前想检查的代码,并将运行的结果与我们的设想结果在程序中进行比较,如果一致,则说明了我们的代码没有问题,是按照预期工作的。

TDD是一种相对于普通思维的方式来说,比较极端的一种做法。我们一般能想到的是先编写业务代码,然后为其编写测试代码,用来验证产品方法是不是按照设计工作。而TDD的思想正好与之相反,在TDD的世界中,我们应该首先根据需求或者接口情况编写测试,然后再根据测试来编写业务代码,而这其实是违反传统软件开发中的先验认知的。但是我们可以举一个生活中类似的例子来说明TDD的必要性:有经验的砌砖师傅总是会先拉一条垂线,然后沿着线砌砖,因为有直线的保证,因此可以做到笔直整齐;而新入行的师傅往往二话不说直接开工,然后在一阶段完成后再用直尺垂线之类的工具进行测量和修补。TDD的好处不言自明,因为总是先测试,再编码,所以至少你的所有代码的public部分都应该含有必要的测试。另外,因为测试代码实际是要使用产品代码的,因此在编写产品代码前你将有一次深入思考和实践如何使用这些代码的机会,这对提高设计和可扩展性有很好的帮助,试想一下你测试都很难写的接口,别人(或者自己)用起来得多纠结。在测试的准绳下,你可以有目的有方向地编码;另外,因为有测试的保护,你可以放心对原有代码进行重构,而不必担心破坏逻辑。这些其实都指向了一个最终的目的:让我们快乐安心高效地工作。

BDD的测试思想

以下同样引自王巍大神的博客:

XCTest(作者注:苹果官方测试框架)是基于OCUnit的传统测试框架,在书写性和可读性上都不太好。在测试用例太多的时候,由于各个测试方法是割裂的,想在某个很长的测试文件中找到特定的某个测试并搞明白这个测试是在做什么并不是很容易的事情。所有的测试都是由断言完成的,而很多时候断言的意义并不是特别的明确,对于项目交付或者新的开发人员加入时,往往要花上很大成本来进行理解或者转换。另外,每一个测试的描述都被写在断言之后,夹杂在代码之中,难以寻找。使用XCTest测试另外一个问题是难以进行mock或者stub,而这在测试中是非常重要的一部分。

行为驱动开发(BDD)正是为了解决上述问题而生的,作为第二代敏捷方法,BDD提倡的是通过将测试语句转换为类似自然语言的描述,开发人员可以使用更符合大众语言的习惯来书写测试,这样不论在项目交接/交付,或者之后自己修改时,都可以顺利很多。如果说作为开发者的我们日常工作是写代码,那么BDD其实就是在讲故事。一个典型的BDD的测试用例包活完整的三段式上下文,测试大多可以翻译为Given..When..Then的格式,读起来轻松惬意。BDD在其他语言中也已经有一些框架,包括最早的Java的JBehave和赫赫有名的Ruby的RSpec和Cucumber。而在objc社区中BDD框架也正在欣欣向荣地发展,得益于objc的语法本来就非常接近自然语言,再加上C语言宏的威力,我们是有可能写出漂亮优美的测试的。在objc中,现在比较流行的BDD框架有cedar,specta和Kiwi。其中个人比较喜欢Kiwi,使用Kiwi写出的测试看起来大概会是这个样子的:

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

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

我们很容易根据上下文将其提取为Given..When..Then的三段式自然语言

Given a team, when newly created, it should have a name, and should have 11 players  

Quick + Nimble In Swift

就像王巍大神在博客中所提到的,iOS和Mac开发中,也诞生了不少很棒的第三方BDD测试框架,如OC时代的:

Swift时代应运而生的:

他们之间的比较和简单介绍,可以参见行为驱动开发

另外,推荐大家观看一下历届WWDC关于测试的视频,有英文字幕.

WWDC关于测试视频.png

接下来,讲一下Quick + Nimble在Swift中的使用,学习自Quick文档.

1. CocoaPods安装Quick + Nimble

如果不喜欢用CocoaPods安装,可以按照文档利用其它方式.

pods描述文件(记得去官网实时更新版本号Quick):

# Podfile

use_frameworks!

def testing_pods

pod 'Quick', '~> 0.8.0'
pod 'Nimble', '3.0.0'

end

target 'MyTests' do
    testing_pods
end

target 'MyUITests' do
    testing_pods
end  
[可选].利用Alcatraz安装Quick测试文件模板

如果不喜欢用Alcatraz安装,可以按照文档利用其它方式.

QuickTemplates.png
2. 使用前,Xcode的相关设置
  • 工程中的defines module设置为YES
�设置defines module.png
  • 用public来修饰需要测试的struck,class等,还有其中的变量和方法
  • 在你的测试Target中导入app target 的module
�导入相应module.png
3. 有效测试的三板斧思路:Arrange, Act, and Assert

我们利用苹果官方XCTest框架来演示这节.
其一,了解一下XCTest,
其二,可以借此体会Quick+Nimble的优势.

相关代码:
Banana.swift

public class Banana {

    private var isPeeled = false
    
    public init() {
    
    }
    
    public func peel() {
    
        isPeeled = true
    }
    
    public var isEdible : Bool {
    
        return isPeeled
    }
}  

BananaTests.swift


import XCTest
import UseQuick

class BananaTest: XCTestCase {
    
    // 为了准确定位测试内容,方法名应该能反映出测试内容
    func testPeel_makesTheBananaEdible()  {
    
        // Arrange:
        let banana = Banana()
        
        // Act:
        banana.peel()
        
        // Assert:
        XCTAssertTrue(banana.isEdible)
    }
}  

Offer.swift

public func offer(banana : Banana) -> String {
    
    if banana.isEdible {
        
        return "Hey, want a banana ?"
        
    } else {
        
        return "Hey, want me to peel a banana for u ?"
    }
}  

OfferTests.swift

import XCTest
import UseQuick

class OfferTests: XCTestCase {
    
    var banana : Banana!
    
    override func setUp() {
        
        super.setUp()
        banana = Banana()
    }
    
    override func tearDown() {
        
        banana = nil
        super.tearDown()
    }
    
    func testOffer_whenTheBananaIsPeeled_offersTheBanana() {
    
        // Arrange:
        banana.peel()
        
        // Act:
        let message = offer(banana)
        
        // Assert:
        XCTAssertEqual(message, "Hey, want a banana ?")
    }
    
    func testOffer_whenTheBananaIsntPeeled_offersToPeelTheBanana() {
        
        // Act:
        let message = offer(banana)
        
        // Assert:
        XCTAssertEqual(message, "Hey, want me to peel a banana for u ?")
    }
    
}  

以上需要注意:

  1. 测试类的后缀一般有命名规范,如苹果官方的测试类文件都以Tests结尾,而Quick以Spec结尾.测试的方法,苹果官方以test作为前缀,这样,编译器就能意识到它是一个测试方法.
  2. 一开始学习测试,三板斧思路:Arrange, Act, and Assert对我们是很有帮助的
  3. 测试方法名应该能反映出测试内容
  4. 苹果官方的测试文件模板给我们提供了setUptearDown方法,就像注释中所说,前者是在所有测试方法执行前调用,后者是所有测试方法执行完毕后调用,我们可以用以管理一些对象的生命周期.
4.Nimble Assertions

为什么要使用Nimble?

Nimble有更简洁,更接近自然语言的语法,更详细的测试信息提示,详见Clearer Tests Using Nimble Assertions

相关代码

Monkey.swift

public enum MonkeyIntelligent {

    case ExtremelySilly
    case NotSilly
    case VerySilly
}

public class Monkey: Equatable {
    
    var name      : String?
    var silliness : MonkeyIntelligent?
    
   public init(name: String, silliness: MonkeyIntelligent) {
    
        self.name      = name
        self.silliness = silliness
    }
}

public func ==(lhs: Monkey, rhs: Monkey) -> Bool {
    
    return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}  

SilliestMonkey.swift

public func silliest(monkeys: [Monkey]) -> [Monkey] {
    
    return monkeys.filter { $0.silliness == .VerySilly || $0.silliness == .ExtremelySilly }
}

public func monkeyContains<T : Equatable>(array : [T], object : T?) -> Bool {

    for temp in array {
    
        if temp == object {
        
            return true
        }
    }
    
    return false
}  

SilliestMonkeyTests.swift

import XCTest
import UseQuick
import Nimble

class SilliestMonkeyTests: XCTestCase {
    
    func testSilliest_whenMonkeysContainSillyMonkeys_theyreIncludedInTheResult() {
        
        // Arrange:
        let kiki = Monkey(name: "Kiki", silliness: .ExtremelySilly)
        let carl = Monkey(name: "Carl", silliness: .NotSilly)
        let jane = Monkey(name: "Jane", silliness: .VerySilly)
        
        // Act:
        let sillyMonkeys = silliest([kiki, carl, jane])
        
        // Assert:
//        XCTAssertTrue(monkeyContains(sillyMonkeys, object: kiki))
//        XCTAssertTrue(monkeyContains(sillyMonkeys,object: kiki), "Expected sillyMonkeys to contain 'Kiki'")
       // 使用Nimble
        expect(sillyMonkeys).to(contain(kiki))
    }
}  
5.Quick

同理,为什么要使用Quick?

还记得在测试中,给方法起那长长的名字么...,比如,前文中的testSilliest_whenMonkeysContainSillyMonkeys_theyreIncludedInTheResult,用Quick,或者其他BDD的框架,就不用在这样做了.

事实上,Quick让我们能够写出更具有描述性的测试,并且,简化我们的代码,尤其是arrange阶段的代码.

it用于描述测试的方法名

import Quick
import Nimble
import Sea

class DolphinSpec: QuickSpec {
  override func spec() {
    it("is friendly") {
      expect(Dolphin().isFriendly).to(beTruthy())
    }

    it("is smart") {
      expect(Dolphin().isSmart).to(beTruthy())
    }
  }
}  

describe用于描述类和方法

import Quick
import Nimble

class DolphinSpec: QuickSpec {
  override func spec() {
    describe("a dolphin") {
      describe("its click") {
        it("is loud") {
          let click = Dolphin().click()
          expect(click.isLoud).to(beTruthy())
        }

        it("has a high frequency") {
          let click = Dolphin().click()
          expect(click.hasHighFrequency).to(beTruthy())
        }
      }
    }
  }
}  

beforeEach/afterEach相当于setUp/tearDown,beforeSuite/afterSuite相当于全局setUp/tearDown

import Quick
import Nimble

class DolphinSpec: QuickSpec {
  override func spec() {
    describe("a dolphin") {
      var dolphin: Dolphin!
      beforeEach {
        dolphin = Dolphin()
      }

      describe("its click") {
        var click: Click!
        beforeEach {
          click = dolphin.click()
        }

        it("is loud") {
          expect(click.isLoud).to(beTruthy())
        }

        it("has a high frequency") {
          expect(click.hasHighFrequency).to(beTruthy())
        }
      }
    }
  }
}  

context用于指定条件或状态

class DolphinSpec: QuickSpec {
  override func spec() {
    describe("a dolphin") {
      var dolphin: Dolphin!
      beforeEach {
        dolphin = Dolphin()
      }

      describe("its click") {
        var click: Click!
        beforeEach {
          click = dolphin.click()
        }

        it("is loud") {
          expect(click.isLoud).to(beTruthy())
        }

        it("has a high frequency") {
          expect(click.hasHighFrequency).to(beTruthy())
        }
      }
    }
  }
}  

我们来对比以下苹果官方用法和Quick用法

苹果:

func testDolphin_click_whenTheDolphinIsNearSomethingInteresting_isEmittedThreeTimes() {
  // ...
}  

Quick:

describe("a dolphin") {
  describe("its click") {
    context("when the dolphin is near something interesting") {
      it("is emitted three times") {
        // ...
      }
    }
  }
}  

由此,Quick的可读性,书写性的优势,可见一斑.

屏蔽测试

在方法名前加'x',可以屏蔽此方法的测试,如:

xdescribe("its click") {
  // ...none of the code in this closure will be run.
}

xcontext("when the dolphin is not near anything interesting") {
  // ...none of the code in this closure will be run.
}

xit("is only emitted once") {
  // ...none of the code in this closure will be run.
}  

集中测试

在方法名前加'f',可以只测试这些加'f'的测试,如:

fit("is loud") {
  // ...only this focused example will be run.
}

it("has a high frequency") {
  // ...this example is not focused, and will not be run.
}

fcontext("when the dolphin is near something interesting") {
  // ...examples in this group are also focused, so they'll be run.
}  

下载源码

下载地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容