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


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

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

  什麼是TDD,爲什麼我們要TDD

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

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

  簡單說來,TDD的基本步驟就是大膽重構

  使用XCTest來執行TDD

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

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


  新建工程

  新建工程後,可以發現在工程中默認已經有一個叫做VVStackTeststarget了,這就是我們測試時使用的target。測試部分的代碼默認放到了{ProjectName}Testsgroup中,現在這個group下有一個測試文件VVStackTests.m。我們的測試例不需要向別的類暴露接口,因此不需要.h文件。另外一般XCTest的測試文件都會以Tests來做文件名結尾。


Test文件和target

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


  失敗的初始測試

VVStackTests.mXcode在新建工程時自動爲我們添加的測試文件。因爲這個文件並不長,所以我們可以將其內容全部抄錄如下:

#import<XCTest/XCTest.h>

@interfaceVVStackTests:XCTestCase

@end

@implementationVVStackTests

-(void)setUp

{

   [supersetUp];

//Putsetupcodehere.Thismethodiscalledbeforetheinvocationofeachtestmethodintheclass.

}

-(void)tearDown

{

//Putteardowncodehere.Thismethodiscalledaftertheinvocationofeachtestmethodintheclass.

   [supertearDown];

}

-(void)testExample

{

XCTFail(@"Noimplementationfor\"%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 TouchObjective-C class,取名VVStack,作爲NSObject的子類。注意在添加的時候,應該只將其加入產品的target中:


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

  由於VVStackNSObject的子類,所以上面的兩個斷言應該都能通過。這時候再運行測試,成功變綠。接下來我們開始考慮這個類的功能:棧的話肯定需要能夠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 {

   return2.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上找到。KiwiBDD的測試思想

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

  行爲驅動開發(BDD)正是爲了解決上述問題而生的,作爲第二代敏捷方法,BDD提倡的是通過將測試語句轉換爲類似自然語言的描述,開發人員可以使用更符合大衆語言的習慣來書寫測試,這樣不論在項目交接/交付,或者之後自己修改時,都可以順利很多。如果說作爲開發者的我們日常工作是寫代碼,那麼BDD其實就是在講故事。一個典型的BDD的測試用例包活完整的三段式上下文,測試大多可以翻譯爲Given..When..Then的格式,讀起來輕鬆愜意。BDD在其他語言中也已經有一些框架,包括最早的JavaJBehave和赫赫有名的RubyRSpecCucumber。而在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 5XCTest環境下,我們需要在Podfile中添加類似下面的條目(記得將VVStackTests換成您自己的項目的測試target的名字):

target :VVStackTests, :exclusive => truedo

pod 'Kiwi/XCTest'

end

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

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

  我們先來新建一個Kiwi測試吧。如果安裝了KiwiTemplate的話,在新建文件中選擇Kiwi/Kiwi Spec來建立一個Specs,取名爲SimpleString,注意選擇目標target爲我們的測試target,模板將會在新建的文件名字後面加上Spec後綴。傳統測試的文件名一般以Tests爲後綴,表示這個文件中含有一組測試,而在Kiwi中,一個測試文件所包含的是一組對於行爲的描述(Spec),因此習慣上使用需要測試的目標類來作爲名字,並以Spec作爲文件名後綴。在Xcode 5中建立測試時已經不會同時創建.h文件了,但是現在的模板中包含有對同名.h的引用,可以在創建後將其刪去。如果您沒有安裝KiwiTemplate的話,可以直接創建一個普通的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的存在,以及其可以嵌套的特性,測試的流程控制相比傳統測試可以更加精確。我們更容易把beforeafter的作用區域限制在合適的地方。

  實際的測試寫在it裏,是由一個一個的期望(Expectations)來進行描述的,期望相當於傳統測試中的斷言,要是運行的結果不能匹配期望,則測試失敗。在Kiwi中期望都由should或者shouldNot開頭,並緊接一個或多個判斷的的鏈式調用,大部分常見的是be或者haveSomeCondition的形式。在我們上面的例子中我們使用了should not be nilshould 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分別是在beforeEachafterEachblock中的賦值的,因此我們需要在聲明時在其前面加上__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的內部實現是一個大量依賴了一個個行爲Matcherobjc的消息轉發,對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現在還在比較快速的發展中,官方repowiki上有一些不錯的資料和文檔,可以參考。VVStack的項目代碼可以在這個repo上找到,可以作爲參考。


本文連接:http://www.blogjava.net/qileilove/archive/2014/02/25/410272.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章