TDD的iOS開發初步以及Kiwi使用入門

測試驅動開發(Test Driven Development,如下簡稱TDD)是保證代碼質量的不二法則,也是先進程序開發的共識。Apple一直致力於在iOS開發中集成更加方便和可用的測試,在Xcode 5中,新的IDE和SDK引入了XCTest來替代原來的SenTestingKit,而且取消了新建工程時的「包括單元測試」的可選項(一樣待遇的還有使用ARC的可選項)。新工程將自動包含測試的target,而且相關框架也搭建完畢,能夠說測試終於擺脫了iOS開發中「二等公民」的地位,如今已經變得和產品代碼同樣重要了。我相信每一個工程師在完成本身的業務代碼的同時,也有最基本的編寫和維護相應的測試代碼的義務,以保證本身的代碼可以正確運行。更進一步,若是可以使用TDD來進行開發,不只能保證代碼運行的正確性,也有助於代碼結構的安排和思考,有助於自身的不斷提升。我在最開始進行開發時也曾對測試嗤之以鼻,但後來無數的慘痛教訓讓我明白那麼多工程師癡迷於測試或者追求更完美的測試,是有其深入含義的。若是您以前尚未開始爲您的代碼編寫測試,我強烈建議,從今天開始,從如今開始(也許作不到的話,也請從下一個項目開始),編寫測試,或者嘗試一下TDD的開發方式。ios

Kiwi是一個iOS平臺十分好用的行爲驅動開發(Behavior Driven Development,如下簡稱BDD)的測試框架,有着很是漂亮的語法,能夠寫出結構性強,很是容易讀懂的測試。由於國內如今有關Kiwi的介紹比較少,加上在測試這塊很能不少工程師們並無特別留意,水平層次可能相差會很遠,所以在這一系列的兩篇博文中,我將從頭開始先簡單地介紹一些TDD的概念和思想,而後從XCTest的最簡單的例子開始,過渡到Kiwi的測試世界。在下一篇中我將繼續深刻介紹一些Kiwi的其餘稍高一些的特性,以期更多的開發者可以接觸並使用Kiwi這個優秀的測試框架。git

什麼是TDD,爲何咱們要TDD

測試驅動開發並非一個很新鮮的概念了。軟件開發工程師們(固然包括你我)最開始學習程序編寫時,最喜歡乾的事情就是編寫一段代碼,而後運行觀察結果是否正確。若是不對就返回代碼檢查錯誤,或者是加入斷點或者輸出跟蹤程序並找出錯誤,而後再次運行查看輸出是否與預想一致。若是輸出只是控制檯的一個簡單的數字或者字符那還好,可是若是輸出必須在點擊一系列按鈕以後才能在屏幕上顯示出來的東西呢?難道咱們就只能一次一次地等待編譯部署,啓動程序而後操做UI,一直點到咱們須要觀察的地方麼?這種行爲無疑是對美好生命和絢麗青春的巨大浪費。因而有一些已經浪費了無數時間的資深工程師們忽然發現,原來咱們能夠在代碼中構建出一個相似的場景,而後在代碼中調用咱們以前想檢查的代碼,並將運行的結果與咱們的設想結果在程序中進行比較,若是一致,則說明了咱們的代碼沒有問題,是按照預期工做的。好比咱們想要實現一個加法函數add,輸入兩個數字,輸出它們相加後的結果。那麼咱們不妨設想咱們真的擁有兩個數,好比3和5,根據人人會的十之內的加法知識,咱們知道答案是8.因而咱們在相加後與預測的8進行比較,若是相等,則說明咱們的函數實現至少對於這個例子是沒有問題的,所以咱們對「這個方法能正確工做」這一命題的信心就增長了。這個例子的僞碼以下:github

//Product Code add(float num1, float num 2) {...} //Test code let a = 3; let b = 5; let c = a + b; if (c == 8) { // Yeah, it works! } else { //Something wrong! } 

當測試足夠全面和具備表明性的時候,咱們即可以信心爆棚,拍着胸脯說,這段代碼沒問題。咱們作出某些條件和假設,並以其爲條件使用到被測試代碼中,並比較預期的結果和實際運行的結果是否相等,這就是軟件開發中測試的基本方式。數組

爲何咱們要test

而TDD是一種相對於普通思惟的方式來講,比較極端的一種作法。咱們通常能想到的是先編寫業務代碼,也就是上面例子中的add方法,而後爲其編寫測試代碼,用來驗證產品方法是否是按照設計工做。而TDD的思想正好與之相反,在TDD的世界中,咱們應該首先根據需求或者接口狀況編寫測試,而後再根據測試來編寫業務代碼,而這實際上是違反傳統軟件開發中的先驗認知的。可是咱們能夠舉一個生活中相似的例子來講明TDD的必要性:有經驗的砌磚師傅老是會先拉一條垂線,而後沿着線砌磚,由於有直線的保證,所以能夠作到筆直整齊;而新入行的師傅每每二話不說直接開工,而後在一階段完成後再用直尺垂線之類的工具進行測量和修補。TDD的好處不言自明,由於老是先測試,再編碼,因此至少你的全部代碼的public部分都應該含有必要的測試。另外,由於測試代碼實際是要使用產品代碼的,所以在編寫產品代碼前你將有一次深刻思考和實踐如何使用這些代碼的機會,這對提升設計和可擴展性有很好的幫助,試想一下你測試都很難寫的接口,別人(或者本身)用起來得多糾結。在測試的準繩下,你能夠有目的有方向地編碼;另外,由於有測試的保護,你能夠放心對原有代碼進行重構,而沒必要擔憂破壞邏輯。這些其實都指向了一個最終的目的:讓咱們快樂安心高效地工做。安全

在TDD原則的指導下,咱們先編寫測試代碼。這時由於尚未對應的產品代碼,因此測試代碼確定是沒法經過的。在大多數測試系統中,咱們使用紅色來表示錯誤,所以一個測試的初始狀態應該是紅色的。接下來咱們須要使用最小的代價(最少的代碼)來讓測試經過。經過的測試將被表示爲安全的綠色,因而咱們回到了綠色的狀態。接下來咱們能夠添加一些測試例,來驗證咱們的產品代碼的實現是否正確。若是不幸新的測試例讓咱們回到了紅色狀態,那咱們就能夠修改產品代碼,使其回到綠色。如此反覆直到各類邊界和測試都進行完畢,此時咱們即可以獲得一個具備測試保證,魯棒性超強的產品代碼。在咱們以後的開發中,由於你有這些測試的保證,你能夠大膽重構這段代碼或者與之相關的代碼,最後只須要保證項目處於綠燈狀態,你就能夠保證代碼沒重構沒有出現問題。ruby

簡單說來,TDD的基本步驟就是「紅→綠→大膽重構」。bash

使用XCTest來執行TDD

Xcode 5中已經集成了XCTest的測試框架(以前版本是SenTestingKit和OCUnit),所謂測試框架,就是一組讓「將測試集成到工程中」以及「編寫和實踐測試」變得簡單的庫。咱們以後將經過實現一個棧數據結構的例子,來用XCTest初步實踐一下TDD開發。在你們對TDD有一些直觀認識以後,再轉到Kiwi的介紹。若是您已經在使用XCTest或者其餘的測試框架了的話,能夠直接跳過本節。數據結構

首先咱們用Xcode新建一個工程吧,選擇模板爲空項目,在Product Name中輸入工程名字VVStack,固然您可使用本身喜歡的名字。若是您使用過Xcode以前的版本的話,應該有留意到以前在這個界面是能夠選擇是否使用Unit Test的,可是如今這個選框已經被取消。框架

新建工程

新建工程後,能夠發如今工程中默認已經有一個叫作VVStackTests的target了,這就是咱們測試時使用的target。測試部分的代碼默認放到了{ProjectName}Tests的group中,如今這個group下有一個測試文件VVStackTests.m。咱們的測試例不須要向別的類暴露接口,所以不須要.h文件。另一般XCTest的測試文件都會以Tests來作文件名結尾。異步

Test文件和target

運行測試的快捷鍵是⌘U(或者可使用菜單的Product→Test),咱們這時候直接對這個空工程進行測試,Xcode在編譯項目後會使用你選擇的設備或者模擬器運行測試代碼。不出意外的話,此次測試將會失敗,如圖:

失敗的初始測試

VVStackTests.m是Xcode在新建工程時自動爲咱們添加的測試文件。由於這個文件並不長,因此咱們能夠將其內容所有抄錄以下:

#import <XCTest/XCTest.h> @interface VVStackTests : XCTestCase @end @implementation VVStackTests - (void)setUp { [super setUp]; // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; } - (void)testExample { XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__); } @end 

能夠看到,VVStackTestsXCTestCase的子類,而XCTestCase正是XCTest測試框架中的測試用例類。XCTest在進行測試時將會尋找測試target中的全部XCTestCase子類,並運行其中以test開頭的全部實例方法。在這裏,默認實現的-testExample將被執行,而在這個方法裏,Xcode默認寫了一個XCTFail的斷言,來強制這個測試失敗,用以提醒咱們測試尚未實現。所謂斷言,就是判斷輸入的條件是否知足。若是不知足,則拋出錯誤並輸出預先規定的字符串做爲提示。在這個Fail的斷言必定會失敗,並提示沒有實現該測試。另外,默認還有兩個方法-setUp-tearDown,正如它們的註釋裏所述,這兩個方法會分別在每一個測試開始和結束的時候被調用。咱們如今正要開始編寫咱們的測試,因此先將原來的-testExample刪除掉。如今再使用⌘U來進行測試,應該能夠順利經過了(由於咱們已經沒有任何測試了)。

接下來讓咱們想一想要作什麼吧。咱們要實現一個簡單的棧數據結構,那麼固然會有一個類來表明這種數據結構,在這個工程中我打算就叫它VVStack。按照常規,咱們能夠新建一個Cocoa Touch類,繼承NSObject而且開始實現了。可是別忘了,咱們如今在TDD,咱們須要先寫測試!那麼首先測試的目標是什麼呢?沒錯,是測試這個VVStack類是否存在,以及是否可以初始化。有了這個目標,咱們就能夠動手開始編寫測試了。在文件開頭加上#import "VVStack.h",而後在VVStackTests.m@end前面加上以下代碼:

- (void)testStackExist { XCTAssertNotNil([VVStack class], @"VVStack class should exist."); } - (void)testStackObjectCanBeCreated { VVStack *stack = [VVStack new]; XCTAssertNotNil(stack, @"VVStack object can be created."); } 

嘛,固然是不可能經過測試的,並且甚至連編譯都沒法完成,由於咱們如今根本沒有一個叫作VVStack的類。最簡單的讓測試經過的方法就是在產品代碼中添加VVStack類。新建一個Cocoa Touch的Objective-C class,取名VVStack,做爲NSObject的子類。注意在添加的時候,應該只將其加入產品的target中:

添加類的時候注意選擇合適的target

因爲VVStack是NSObject的子類,因此上面的兩個斷言應該都能經過。這時候再運行測試,成功變綠。接下來咱們開始考慮這個類的功能:棧的話確定須要可以push,而且push後的棧頂元素應該就是剛纔所push進去的元素。那麼創建一個push方法的測試吧,在剛纔添加的代碼之下繼續寫:

- (void)testPushANumberAndGetIt { VVStack *stack = [VVStack new]; [stack push:2.3]; double topNumber = [stack top]; XCTAssertEqual(topNumber, 2.3, @"VVStack should can be pushed and has that top value."); } 

由於咱們尚未實現-push:-top方法,因此測試毫無疑問地失敗了(在ARC環境中直接沒法編譯)。爲了使測試當即經過咱們首先須要在VVStack.h中聲明這兩個方法,而後在.m的實現文件中進行實現。令測試經過的最簡單的實現是一個空的push方法以及直接返回2.3這個數:

//VVStack.h @interface VVStack : NSObject - (void)push:(double)num; - (double)top; @end //VVStack.m @implementation VVStack - (void)push:(double)num { } - (double)top { return 2.3; } @end 

再次運行測試,咱們順利回到了綠燈狀態。也許你很快就會說,這算哪門子實現啊,若是再增長一組測試例,好比push一個4.6,而後檢查top,不就失敗了麼?咱們難道不該該直接實現一個真正的合理的實現麼?對此的回答是,在實際開發中,咱們確定不會以這樣的步伐來處理像例子中這樣相似的簡單問題,而是會直接跳過一些error-try的步驟,實現一個比較完整的方案。可是在更多的時候,咱們所關心和須要實現的目標並非這樣容易。特別是在對TDD還不熟悉的時候,咱們有必要放慢節奏和動做,將整個開發理念進行充分實踐,這樣纔有可能在以後更復雜的案例中正確使用。因而咱們發揚不怕繁雜,精益求精的精神,在剛纔的測試例上增長一個測試,回到VVStackTests.m中,在剛纔的測試方法中加上:

- (void)testPushANumberAndGetIt { //... [stack push:4.6]; topNumber = [stack top]; XCTAssertEqual(topNumber, 4.6, @"Top value of VVStack should be the last num pushed into it"); } 

很好,這下子咱們回到了紅燈狀態,這正是咱們所指望的,如今是時候來考慮實現這個棧了。這個實現過於簡單,也有很是多的思路,其中一種是使用一個NSMutableArray來存儲數據,而後在top方法裏返回最後加入的數據。修改VVStack.m,加入數組,更改實現:

//VVStack.m @interface VVStack() @property (nonatomic, strong) NSMutableArray *numbers; @end @implementation VVStack - (id)init { if (self = [super init]) { _numbers = [NSMutableArray new]; } return self; } - (void)push:(double)num { [self.numbers addObject:@(num)]; } - (double)top { return [[self.numbers lastObject] doubleValue]; } @end 

測試經過,注意到在-testStackObjectCanBeCreatedtestPushANumberAndGetIt兩個測試中都生成了一個VVStack對象。在這個測試文件中基本每一個測試都會須要初始化對象,所以咱們能夠考慮在測試文件中添加一個VVStack的實例成員,並將測試中的初始化代碼移到-setUp中,並在-tearDown中釋放。

接下來咱們能夠模仿繼續實現pop等棧的方法。鑑於篇幅這裏再也不繼續詳細實現,你們能夠本身動手試試看。記住先實現測試,而後再實現產品代碼。一開始您可能會以爲這很無聊,效率低下,可是請記住這是起步練習不可缺乏的一部分,並且在咱們的例子中其實一切都是以「慢動做」在進行的。相信在通過實踐和使用後,您將會逐漸掌握本身的節奏和重點測試。關於使用XCTest到這裏爲止的代碼,能夠在github上找到。

Kiwi和BDD的測試思想

XCTest是基於OCUnit的傳統測試框架,在書寫性和可讀性上都不太好。在測試用例太多的時候,因爲各個測試方法是割裂的,想在某個很長的測試文件中找到特定的某個測試並搞明白這個測試是在作什麼並非很容易的事情。全部的測試都是由斷言完成的,而不少時候斷言的意義並非特別的明確,對於項目交付或者新的開發人員加入時,每每要花上很大成原本進行理解或者轉換。另外,每個測試的描述都被寫在斷言以後,夾雜在代碼之中,難以尋找。使用XCTest測試另一個問題是難以進行mock或者stub,而這在測試中是很是重要的一部分(關於mock測試的問題,我會在下一篇中繼續深刻)。

行爲驅動開發(BDD)正是爲了解決上述問題而生的,做爲第二代敏捷方法,BDD提倡的是經過將測試語句轉換爲相似天然語言的描述,開發人員可使用更符合大衆語言的習慣來書寫測試,這樣不論在項目交接/交付,或者以後本身修改時,均可以順利不少。若是說做爲開發者的咱們平常工做是寫代碼,那麼BDD其實就是在講故事。一個典型的BDD的測試用例包活完整的三段式上下文,測試大多能夠翻譯爲Given..When..Then的格式,讀起來輕鬆愜意。BDD在其餘語言中也已經有一些框架,包括最先的Java的JBehave和赫赫有名的Ruby的RSpecCucumber。而在objc社區中BDD框架也正在欣欣向榮地發展,得益於objc的語法原本就很是接近天然語言,再加上C語言宏的威力,咱們是有可能寫出漂亮優美的測試的。在objc中,如今比較流行的BDD框架有cedarspectaKiwi。其中我的比較喜歡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

很簡單啊有木有!在這樣的語法下,是否是寫測試的興趣都被激發出來了呢。關於Kiwi的進一步語法和使用,咱們稍後詳細展開。首先來看看如何在項目中添加Kiwi框架吧。

在項目中添加Kiwi

最簡單和最推薦的方法固然是CocoaPods,若是您對CocoaPods還比較陌生的話,推薦您花時間先看一看這篇CocoaPods的簡介。Xcode 5和XCTest環境下,咱們須要在Podfile中添加相似下面的條目(記得將VVStackTests換成您本身的項目的測試target的名字):

target :VVStackTests, :exclusive => true do pod 'Kiwi/XCTest' end 

以後pod install之後,打開生成的xcworkspace文件,Kiwi就已經處於可用狀態了。另外,爲了咱們在新建測試的時候能省點事兒,能夠在官方repo裏下載並運行安裝Kiwi的Xcode Template。若是您堅持不用CocoaPods,而想要本身進行配置Kiwi的話,能夠參考這篇wiki

行爲描述(Specs)和指望(Expectations),Kiwi測試的基本結構

咱們先來新建一個Kiwi測試吧。若是安裝了Kiwi的Template的話,在新建文件中選擇Kiwi/Kiwi Spec來創建一個Specs,取名爲SimpleString,注意選擇目標target爲咱們的測試target,模板將會在新建的文件名字後面加上Spec後綴。傳統測試的文件名通常以Tests爲後綴,表示這個文件中含有一組測試,而在Kiwi中,一個測試文件所包含的是一組對於行爲的描述(Spec),所以習慣上使用須要測試的目標類來做爲名字,並以Spec做爲文件名後綴。在Xcode 5中創建測試時已經不會同時建立.h文件了,可是如今的模板中包含有對同名.h的引用,能夠在建立後將其刪去。若是您沒有安裝Kiwi的Template的話,能夠直接建立一個普通的Objective-C test case class,而後將內容替換爲下面這樣:

#import <Kiwi/Kiwi.h> SPEC_BEGIN(SimpleStringSpec) describe(@"SimpleString", ^{ }); SPEC_END 

你可能會以爲這不是objc代碼,甚至懷疑這些語法是否可以編譯經過。其實SPEC_BEGINSPEC_END都是宏,它們定義了一個KWSpec的子類,並將其中的內容包裝在一個函數中(有興趣的朋友不妨點進去看看)。咱們如今先添加一些描述和測試語句,並運行看看吧,將上面的代碼的SPEC_BEGINSPEC_END之間的內容替換爲:

describe(@"SimpleString", ^{ context(@"when assigned to 'Hello world'", ^{ NSString *greeting = @"Hello world"; it(@"should exist", ^{ [[greeting shouldNot] beNil]; }); it(@"should equal to 'Hello world'", ^{ [[greeting should] equal:@"Hello world"]; }); }); }); 

describe描述須要測試的對象內容,也即咱們三段式中的Givencontext描述測試上下文,也就是這個測試在When來進行,最後it中的是測試的本體,描述了這個測試應該知足的條件,三者共同構成了Kiwi測試中的行爲描述。它們是能夠nest的,也就是一個Spec文件中能夠包含多個describe(雖然咱們不多這麼作,一個測試文件應該專一於測試一個類);一個describe能夠包含多個context,來描述類在不一樣情景下的行爲;一個context能夠包含多個it的測試例。讓咱們運行一下這個測試,觀察輸出:

VVStack[36517:70b] + 'SimpleString, when assigned to 'Hello world', should exist' [PASSED] VVStack[36517:70b] + 'SimpleString, when assigned to 'Hello world', should equal to 'Hello world'' [PASSED] 

能夠看到,這三個關鍵字的描述將在測試時被依次打印出來,造成一個完整的行爲描述。除了這三個以外,Kiwi還有一些其餘的行爲描述關鍵字,其中比較重要的包括

  • beforeAll(aBlock) - 當前scope內部的全部的其餘block運行以前調用一次
  • afterAll(aBlock) - 當前scope內部的全部的其餘block運行以後調用一次
  • beforeEach(aBlock) - 在scope內的每一個it以前調用一次,對於context的配置代碼應該寫在這裏
  • afterEach(aBlock) - 在scope內的每一個it以後調用一次,用於清理測試後的代碼
  • specify(aBlock) - 能夠在裏面直接書寫不須要描述的測試
  • pending(aString, aBlock) - 只打印一條log信息,不作測試。這個語句會給出一條警告,能夠做爲一開始集中書寫行爲描述時還未實現的測試的提示。
  • xit(aString, aBlock) - 和pending同樣,另外一種寫法。由於在真正實現時測試時只須要將x刪掉就是it,可是pending語意更明確,所以仍是推薦pending

能夠看到,因爲有context的存在,以及其能夠嵌套的特性,測試的流程控制相比傳統測試能夠更加精確。咱們更容易把before和after的做用區域限制在合適的地方。

實際的測試寫在it裏,是由一個一個的指望(Expectations)來進行描述的,指望至關於傳統測試中的斷言,要是運行的結果不能匹配指望,則測試失敗。在Kiwi中指望都由should或者shouldNot開頭,並緊接一個或多個判斷的的鏈式調用,大部分常見的是be或者haveSomeCondition的形式。在咱們上面的例子中咱們使用了should not be nil和should equal兩個指望來確保字符串賦值的行爲正確。其餘的指望語句很是豐富,而且都符合天然語言描述,因此並不須要太多介紹。在使用的時候不妨直接按照本身的想法來描述本身的指望,通常狀況下在IDE的幫助下咱們都能找到想要的結果。若是您想看看完整的指望語句的列表,能夠參看文檔的這個頁面。另外,您還能夠經過新建KWMatcher的子類,來簡單地自定義本身和項目所須要的指望語句。從這一點來看,Kiwi能夠說是一個很是靈活並具備可擴展性的測試框架。

到此爲止的代碼能夠從這裏找到。

Kiwi實際使用實例

最後咱們來用Kiwi完整地實現VVStack類的測試和開發吧。首先重寫剛纔XCTest的相關測試:新建一個VVStackSpec做爲Kiwi版的測試用例,而後把describe換成下面的代碼:

describe(@"VVStack", ^{ context(@"when created", ^{ __block VVStack *stack = nil; beforeEach(^{ stack = [VVStack new]; }); afterEach(^{ stack = nil; }); it(@"should have the class VVStack", ^{ [[[VVStack class] shouldNot] beNil]; }); it(@"should exist", ^{ [[stack shouldNot] beNil]; }); it(@"should be able to push and get top", ^{ [stack push:2.3]; [[theValue([stack top]) should] equal:theValue(2.3)]; [stack push:4.6]; [[theValue([stack top]) should] equal:4.6 withDelta:0.001]; }); }); }); 

看到這裏的您看這段測試應該不成問題。須要注意的有兩點:首先stack分別是在beforeEachafterEach的block中的賦值的,所以咱們須要在聲明時在其前面加上__block標誌。其次,指望描述的should或者shouldNot是做用在對象上的宏,所以對於標量,咱們須要先將其轉換爲對象。Kiwi爲咱們提供了一個標量轉對象的語法糖,叫作theValue,在作精確比較的時候咱們能夠直接使用例子中直接與2.3作比較這樣的寫法來進行對比。可是若是測試涉及到運算的話,因爲浮點數精度問題,咱們通常使用帶有精度的比較指望來進行描述,即4.6例子中的equal:withDelta:(固然,這裏只是爲了demo,實際在這用和上面2.3同樣的方法就行了)。

接下來咱們再爲這個context添加一個測試例,用來測試初始情況時棧是否爲空。由於咱們使用了一個Array來做爲存儲容器,根據咱們以前用過的equal方法,咱們很容易想到下面這樣的測試代碼

it(@"should equal contains 0 element", ^{ [[theValue([stack.numbers count]) should] equal:theValue(0)]; }); 

這段測試在邏輯上沒有太大問題,可是有很是多值得改進的地方。首先若是咱們須要將原來寫在Extension裏的numbers暴露到頭文件中,這對於類的封裝是一種破壞,對於這個,一種常見的作法是隻暴露一個-count方法,讓其返回numbers的元素個數,從而保證numbers的私有性。另外對於取值和轉換,其實theValue的存在在必定程度上是破壞了測試可讀性的,咱們能夠想辦法改善一下,好比對於0的來講,咱們有beZero這樣的指望可使用。簡單改寫之後,這個VVStack.h和這個測試能夠變成這個樣子:

//VVStack.h //... - (NSUInteger)count; //... //VVStack.m //... - (NSUInteger)count { return [self.numbers count]; } //... it(@"should equal contains 0 element", ^{ [[theValue([stack count]) should] beZero]; }); 

更進一步地,對於一個collection來講,Kiwi有一些特殊處理,好比havehaveCountOf系列的指望。若是測試的對象實現了-count方法的話,咱們就可使用這一系列指望來寫出更好的測試語句。好比上面的測試還能夠進一步寫成

it(@"should equal contains 0 element", ^{ [[stack should] haveCountOf:0]; }); 

在這種狀況下,咱們並無顯式地調用VVStack的-count方法,因此咱們能夠在頭文件中將其刪掉。可是咱們須要保留這個方法的實現,由於測試時是須要這個方法的。若是測試對象不能響應count方法的話,如你所料,測試時會扔一個unrecognized selector的錯。Kiwi的內部實現是一個大量依賴了一個個行爲Matcher和objc的消息轉發,對objcruntime特性比較熟悉,並想更深刻的朋友不放能夠看看Kiwi的源碼,寫得至關漂亮。

其實對於這個測試,咱們還能夠寫出更漂亮的版本,像這樣:

it(@"should equal contains 0 element", ^{ [[stack should] beEmpty]; }); 

好了。關於空棧這個情景下的測試感受差很少了。咱們繼續用TDD的思想來完善VVStack類吧。棧的話,咱們固然須要可以-pop,也就是說在(Given)給定一個棧時,(When)當棧中有元素的時候,(Then)咱們能夠pop它,而且獲得棧頂元素。咱們新建一個context,而後按照這個思路書寫行爲描述(測試):

context(@"when new created and pushed 4.6", ^{ __block VVStack *stack = nil; beforeEach(^{ stack = [VVStack new]; [stack push:4.6]; }); afterEach(^{ stack = nil; }); it(@"can be poped and the value equals 4.6", ^{ [[theValue([stack pop]) should] equal:theValue(4.6)]; }); it(@"should contains 0 element after pop", ^{ [stack pop]; [[stack should] beEmpty]; }); }); 

完成了測試書寫後,咱們開始按照設計填寫產品代碼。在VVStack.h中完成申明,並在.m中加入相應實現。

- (double)pop { double result = [self top]; [self.numbers removeLastObject]; return result; } 

很簡單吧。並且由於有測試的保證,咱們在提供像Stack這樣的基礎類時,就不須要等到或者在真實的環境中檢測了。由於在被別人使用以前,咱們本身的測試代碼已經可以保證它的正確性了。VVStack剩餘的最後一個小問題是,在棧是空的時候,咱們執行pop操做時應該給出一個錯誤,用以提示空棧沒法pop。雖然在objc中異常並不常見,可是在這個情景下是拋異常的好時機,也符合通常C語言對於出空棧的行爲。咱們能夠在以前的「when created」上下文中加入一個指望:

it(@"should raise a exception when pop", ^{ [[theBlock(^{ [stack pop]; }) should] raiseWithName:@"VVStackPopEmptyException"]; }); 

theValue配合標量值相似,theBlock也是Kiwi中的一個轉換語法,用來將一段程序轉換爲相應的matcher,使其能夠被施加指望。這裏咱們指望空的Stack在被pop時拋出一個叫作"VVStackPopEmptyException"的異常。咱們能夠重構pop方法,在棧爲空時給一個異常:

- (double)pop { if ([self count] == 0) { [NSException raise:@"VVStackPopEmptyException" format:@"Can not pop an empty stack."]; } double result = [self top]; [self.numbers removeLastObject]; return result; } 

進一步的Kiwi

VVStack的測試和實現就到這裏吧,根據這套測試,您可使用本身的實現來輕易地重構這個類,而沒必要擔憂破壞它的公共接口的行爲。若是須要添加新的功能或者修正已有bug的時候,咱們也能夠經過添加或者修改相應的測試,來確保正確性。我將會在下一篇博文中繼續介紹Kiwi,看看Kiwi在異步測試和mock/stub的使用和表現如何。Kiwi如今還在比較快速的發展中,官方repo的wiki上有一些不錯的資料和文檔,能夠參考。VVStack的項目代碼能夠在這個repo上找到,能夠做爲參考。

相關文章
相關標籤/搜索