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

學(xué)習(xí)文章

TDD的必要性

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

測(cè)試驅(qū)動(dòng)開發(fā)(Test Driven Development碟摆,以下簡(jiǎn)稱TDD)是保證代碼質(zhì)量的不二法則,也是先進(jìn)程序開發(fā)的共識(shí)谋国。

測(cè)試驅(qū)動(dòng)開發(fā)并不是一個(gè)很新鮮的概念了膊夹。軟件開發(fā)工程師們(當(dāng)然包括你我)最開始學(xué)習(xí)程序編寫時(shí)谨设,最喜歡干的事情就是編寫一段代碼幽勒,然后運(yùn)行觀察結(jié)果是否正確洞翩。如果不對(duì)就返回代碼檢查錯(cuò)誤,或者是加入斷點(diǎn)或者輸出跟蹤程序并找出錯(cuò)誤沮翔,然后再次運(yùn)行查看輸出是否與預(yù)想一致艺普。如果輸出只是控制臺(tái)的一個(gè)簡(jiǎn)單的數(shù)字或者字符那還好,但是如果輸出必須在點(diǎn)擊一系列按鈕之后才能在屏幕上顯示出來的東西呢鉴竭?難道我們就只能一次一次地等待編譯部署,啟動(dòng)程序然后操作UI岸浑,一直點(diǎn)到我們需要觀察的地方么搏存?這種行為無疑是對(duì)美好生命和絢麗青春的巨大浪費(fèi)。于是有一些已經(jīng)浪費(fèi)了無數(shù)時(shí)間的資深工程師們突然發(fā)現(xiàn)矢洲,原來我們可以在代碼中構(gòu)建出一個(gè)類似的場(chǎng)景璧眠,然后在代碼中調(diào)用我們之前想檢查的代碼,并將運(yùn)行的結(jié)果與我們的設(shè)想結(jié)果在程序中進(jìn)行比較读虏,如果一致责静,則說明了我們的代碼沒有問題,是按照預(yù)期工作的盖桥。

TDD是一種相對(duì)于普通思維的方式來說灾螃,比較極端的一種做法。我們一般能想到的是先編寫業(yè)務(wù)代碼揩徊,然后為其編寫測(cè)試代碼腰鬼,用來驗(yàn)證產(chǎn)品方法是不是按照設(shè)計(jì)工作嵌赠。而TDD的思想正好與之相反,在TDD的世界中熄赡,我們應(yīng)該首先根據(jù)需求或者接口情況編寫測(cè)試姜挺,然后再根據(jù)測(cè)試來編寫業(yè)務(wù)代碼,而這其實(shí)是違反傳統(tǒng)軟件開發(fā)中的先驗(yàn)認(rèn)知的彼硫。但是我們可以舉一個(gè)生活中類似的例子來說明TDD的必要性:有經(jīng)驗(yàn)的砌磚師傅總是會(huì)先拉一條垂線炊豪,然后沿著線砌磚,因?yàn)橛兄本€的保證拧篮,因此可以做到筆直整齊词渤;而新入行的師傅往往二話不說直接開工,然后在一階段完成后再用直尺垂線之類的工具進(jìn)行測(cè)量和修補(bǔ)他托。TDD的好處不言自明掖肋,因?yàn)榭偸窍葴y(cè)試,再編碼赏参,所以至少你的所有代碼的public部分都應(yīng)該含有必要的測(cè)試志笼。另外,因?yàn)闇y(cè)試代碼實(shí)際是要使用產(chǎn)品代碼的把篓,因此在編寫產(chǎn)品代碼前你將有一次深入思考和實(shí)踐如何使用這些代碼的機(jī)會(huì)纫溃,這對(duì)提高設(shè)計(jì)和可擴(kuò)展性有很好的幫助,試想一下你測(cè)試都很難寫的接口韧掩,別人(或者自己)用起來得多糾結(jié)紊浩。在測(cè)試的準(zhǔn)繩下,你可以有目的有方向地編碼疗锐;另外坊谁,因?yàn)橛袦y(cè)試的保護(hù),你可以放心對(duì)原有代碼進(jìn)行重構(gòu)滑臊,而不必?fù)?dān)心破壞邏輯口芍。這些其實(shí)都指向了一個(gè)最終的目的:讓我們快樂安心高效地工作。

BDD的測(cè)試思想

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

XCTest(作者注:蘋果官方測(cè)試框架)是基于OCUnit的傳統(tǒng)測(cè)試框架雇卷,在書寫性和可讀性上都不太好鬓椭。在測(cè)試用例太多的時(shí)候,由于各個(gè)測(cè)試方法是割裂的关划,想在某個(gè)很長(zhǎng)的測(cè)試文件中找到特定的某個(gè)測(cè)試并搞明白這個(gè)測(cè)試是在做什么并不是很容易的事情小染。所有的測(cè)試都是由斷言完成的,而很多時(shí)候斷言的意義并不是特別的明確贮折,對(duì)于項(xiàng)目交付或者新的開發(fā)人員加入時(shí)裤翩,往往要花上很大成本來進(jìn)行理解或者轉(zhuǎn)換。另外调榄,每一個(gè)測(cè)試的描述都被寫在斷言之后岛都,夾雜在代碼之中律姨,難以尋找。使用XCTest測(cè)試另外一個(gè)問題是難以進(jìn)行mock或者stub臼疫,而這在測(cè)試中是非常重要的一部分择份。

行為驅(qū)動(dòng)開發(fā)(BDD)正是為了解決上述問題而生的,作為第二代敏捷方法烫堤,BDD提倡的是通過將測(cè)試語句轉(zhuǎn)換為類似自然語言的描述荣赶,開發(fā)人員可以使用更符合大眾語言的習(xí)慣來書寫測(cè)試,這樣不論在項(xiàng)目交接/交付鸽斟,或者之后自己修改時(shí)拔创,都可以順利很多。如果說作為開發(fā)者的我們?nèi)粘9ぷ魇菍懘a富蓄,那么BDD其實(shí)就是在講故事剩燥。一個(gè)典型的BDD的測(cè)試用例包活完整的三段式上下文,測(cè)試大多可以翻譯為Given..When..Then的格式立倍,讀起來輕松愜意灭红。BDD在其他語言中也已經(jīng)有一些框架,包括最早的Java的JBehave和赫赫有名的Ruby的RSpec和Cucumber口注。而在objc社區(qū)中BDD框架也正在欣欣向榮地發(fā)展变擒,得益于objc的語法本來就非常接近自然語言,再加上C語言宏的威力寝志,我們是有可能寫出漂亮優(yōu)美的測(cè)試的娇斑。在objc中,現(xiàn)在比較流行的BDD框架有cedar材部,specta和Kiwi毫缆。其中個(gè)人比較喜歡Kiwi,使用Kiwi寫出的測(cè)試看起來大概會(huì)是這個(gè)樣子的:

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];
        });
    });
});  

我們很容易根據(jù)上下文將其提取為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開發(fā)中,也誕生了不少很棒的第三方BDD測(cè)試框架,如OC時(shí)代的:

Swift時(shí)代應(yīng)運(yùn)而生的:

他們之間的比較和簡(jiǎn)單介紹,可以參見行為驅(qū)動(dòng)開發(fā)

另外,推薦大家觀看一下歷屆WWDC關(guān)于測(cè)試的視頻,有英文字幕.

WWDC關(guān)于測(cè)試視頻.png

接下來,講一下Quick + Nimble在Swift中的使用,學(xué)習(xí)自Quick文檔.

1. CocoaPods安裝Quick + Nimble

如果不喜歡用CocoaPods安裝,可以按照文檔利用其它方式.

pods描述文件(記得去官網(wǎng)實(shí)時(shí)更新版本號(hào)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測(cè)試文件模板

如果不喜歡用Alcatraz安裝,可以按照文檔利用其它方式.

QuickTemplates.png
2. 使用前,Xcode的相關(guān)設(shè)置
  • 工程中的defines module設(shè)置為YES
?設(shè)置defines module.png
  • 用public來修飾需要測(cè)試的struck,class等,還有其中的變量和方法
  • 在你的測(cè)試Target中導(dǎo)入app target 的module
?導(dǎo)入相應(yīng)module.png
3. 有效測(cè)試的三板斧思路:Arrange, Act, and Assert

我們利用蘋果官方XCTest框架來演示這節(jié).
其一,了解一下XCTest,
其二,可以借此體會(huì)Quick+Nimble的優(yōu)勢(shì).

相關(guān)代碼:
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 {
    
    // 為了準(zhǔn)確定位測(cè)試內(nèi)容,方法名應(yīng)該能反映出測(cè)試內(nèi)容
    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. 測(cè)試類的后綴一般有命名規(guī)范,如蘋果官方的測(cè)試類文件都以Tests結(jié)尾,而Quick以Spec結(jié)尾.測(cè)試的方法,蘋果官方以test作為前綴,這樣,編譯器就能意識(shí)到它是一個(gè)測(cè)試方法.
  2. 一開始學(xué)習(xí)測(cè)試,三板斧思路:Arrange, Act, and Assert對(duì)我們是很有幫助的
  3. 測(cè)試方法名應(yīng)該能反映出測(cè)試內(nèi)容
  4. 蘋果官方的測(cè)試文件模板給我們提供了setUptearDown方法,就像注釋中所說,前者是在所有測(cè)試方法執(zhí)行前調(diào)用,后者是所有測(cè)試方法執(zhí)行完畢后調(diào)用,我們可以用以管理一些對(duì)象的生命周期.
4.Nimble Assertions

為什么要使用Nimble?

Nimble有更簡(jiǎn)潔,更接近自然語言的語法,更詳細(xì)的測(cè)試信息提示,詳見Clearer Tests Using Nimble Assertions

相關(guān)代碼

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?

還記得在測(cè)試中,給方法起那長(zhǎng)長(zhǎng)的名字么...,比如,前文中的testSilliest_whenMonkeysContainSillyMonkeys_theyreIncludedInTheResult,用Quick,或者其他BDD的框架,就不用在這樣做了.

事實(shí)上,Quick讓我們能夠?qū)懗龈哂忻枋鲂缘臏y(cè)試,并且,簡(jiǎn)化我們的代碼,尤其是arrange階段的代碼.

it用于描述測(cè)試的方法名

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相當(dāng)于setUp/tearDown,beforeSuite/afterSuite相當(dāng)于全局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用于指定條件或狀態(tài)

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())
        }
      }
    }
  }
}  

我們來對(duì)比以下蘋果官方用法和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的可讀性,書寫性的優(yōu)勢(shì),可見一斑.

屏蔽測(cè)試

在方法名前加'x',可以屏蔽此方法的測(cè)試,如:

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

集中測(cè)試

在方法名前加'f',可以只測(cè)試這些加'f'的測(cè)試,如:

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

下載源碼

下載地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末乐导,一起剝皮案震驚了整個(gè)濱河市悔醋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兽叮,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猾愿,死亡現(xiàn)場(chǎng)離奇詭異鹦聪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蒂秘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門泽本,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人姻僧,你說我怎么就攤上這事规丽∑涯粒” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵赌莺,是天一觀的道長(zhǎng)冰抢。 經(jīng)常有香客問我,道長(zhǎng)艘狭,這世上最難降的妖魔是什么挎扰? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮巢音,結(jié)果婚禮上遵倦,老公的妹妹穿的比我還像新娘。我一直安慰自己官撼,他們只是感情好梧躺,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著傲绣,像睡著了一般掠哥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上斜筐,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天龙致,我揣著相機(jī)與錄音,去河邊找鬼顷链。 笑死目代,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嗤练。 我是一名探鬼主播榛了,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼煞抬!你這毒婦竟也來了霜大?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤革答,失蹤者是張志新(化名)和其女友劉穎战坤,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體残拐,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡途茫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了溪食。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片囊卜。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出栅组,到底是詐尸還是另有隱情雀瓢,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布玉掸,位于F島的核電站刃麸,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏排截。R本人自食惡果不足惜嫌蚤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望断傲。 院中可真熱鬧脱吱,春花似錦、人聲如沸认罩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽垦垂。三九已至宦搬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間劫拗,已是汗流浹背间校。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留页慷,地道東北人憔足。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像酒繁,于是被迫代替她去往敵國和親滓彰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容