TDD和BDD
在GitBook上看過一篇文章,一個不寫單元測試的程序員不是一個好的攻城獅。坦白的說,在Objective-C這個領域的里,我見過的會主動寫單元測試的程序員還是比較少的。當然了,在那些大的開源項目里,我還是見到過很多單元測試的應用。
于是也就促使我想總結總結自己現在對單元測試的理解。眾所周知蘋果在Xcode5
中引入了XCTest
框架替換了原來的SenTestingKit
。這也顯示了蘋果一直致力于在iOS開發中集成更方便可用的測試。但是我一直覺得XCTest
的斷言可讀性較差,如果是讓他人來閱讀這段單元測試,會比較的花費精力。
再進入討論單元測試之前,我們來談談不一樣測試思想
行為驅動開發(英語:Behavior-driven development,縮寫BDD)是一種敏捷軟件開發的技術,BDD的重點是通過與利益相關者的討論取得對預期的軟件行為的清醒認識。它通過用自然語言書寫非程序員可讀的測試用例擴展了測試驅動開發方法。
測試驅動開發(英語:Test-driven development,縮寫為TDD)是一種軟件開發過程中的應用方法,由極限編程中倡導,以其倡導先寫測試程序,然后編碼實現其功能得名。測試驅動開發是戴兩頂帽子思考的開發方式:先戴上實現功能的帽子,在測試的輔助下,快速實現其功能;再戴上重構的帽子,在測試的保護下,通過去除冗余的代碼,提高代碼質量。測試驅動著整個開發過程:首先,驅動代碼的設計和功能的實現;其后,驅動代碼的再設計和重構。
上面講述了TDD和BDD的思想差別,看到這里,你們認為當前的iOS開發適合怎樣的測試思想。不知道你們開發中的實際情況是如何,在現在大環境趕進度的開發下,一般我是采用BDD的測試方法。
而談到BDD,我要給大家介紹一個iOS中非常有名并且好用的BDD框架 —— Kiwi。
Kiwi
Kiwi的安裝
使用Cocopods 安裝
target :YourProjectTests do
pod 'Kiwi'
end
在這里記得一定要替換YourProject
為你的項目名。
Kiwi的基本結構
在講Kiwi的常用語法前,我們先來看一段Kiwi的Github提供的示例代碼。
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
是不是非常簡單易懂的語法結構。
describe
描述需要測試的對象內容,也即我們三段式中的Given
,context
描述測試上下文,也就是這個測試在When
來進行,最后it
中的是測試的本體,描述了這個測試應該滿足的條件,三者共同構成了Kiwi測試中的行為描述。它們是可以nest的,也就是一個Spec文件中可以包含多個describe
(雖然我們很少這么做,一個測試文件應該專注于測試一個類);一個describe
可以包含多個context
,來描述類在不同情景下的行為;一個context
可以包含多個it
的測試例。
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
Kiwi使用實例
就拿項目中一個真實的場景來說,我在寫完一個適配所有iPhone機型的寬高的類之后,我用Kiwi來進行單元測試。
首先我這個類是這么描述寬高的
//CalculateLayout.h
+ (CGFloat)neu_layoutForAlliPhoneHeight:(CGFloat)height;
+ (CGFloat)neu_layoutForAlliPhoneWidth:(CGFloat)width;
// CalculateLayout.m
+ (CGFloat)layoutForAlliPhoneHeight:(CGFloat)height type:(IPhoneType)type {
CGFloat layoutHeight = 0.0f;
switch (type) {
case iPhone4Type:
layoutHeight = ( height / iPhone6Height ) * iPhone4Height;
break;
case iPhone5Type:
layoutHeight = ( height / iPhone6Height ) * iPhone5Height;
break;
case iPhone6Type:
layoutHeight = ( height / iPhone6Height ) * iPhone6Height;
break;
case iPhone6PlusType:
layoutHeight = ( height / iPhone6Height ) * iPhone6PlusHeight;
break;
default:
break;
}
return layoutHeight;
}
+ (CGFloat)layoutForAlliPhoneWidth:(CGFloat)width type:(IPhoneType)type {
CGFloat layoutWidth = 0.0f;
switch (type) {
case iPhone4Type:
layoutWidth = ( width / iPhone6Width ) * iPhone4Width;
break;
case iPhone5Type:
layoutWidth = ( width / iPhone6Width ) * iPhone5Width;
break;
case iPhone6Type:
layoutWidth = ( width / iPhone6Width ) * iPhone6Width;
break;
case iPhone6PlusType:
layoutWidth = ( width / iPhone6Width ) * iPhone6PlusWidth;
break;
default:
break;
}
return layoutWidth;
}
反正大概意思就是我輸入了一個寬高,他根據UI給定的設計圖,返回給我一個寬高適配當前機型的寬高。
那么我們如何來寫這個測試用例呢.
#import <Kiwi/Kiwi.h>
#import "CalculateLayout.h"
SPEC_BEGIN(CalculateLayoutTests)
describe(@"CalculateLayout", ^{
context(@"when calculate width and height", ^{
CGFloat width = [CalculateLayout neu_layoutForAlliPhoneWidth:375.f];
CGFloat height = [CalculateLayout neu_layoutForAlliPhoneHeight:667.f];
pending_(@"All iPhone Test", ^{
});
it(@"should layout width", ^{
[[theValue(width) should] equal:theValue(320.f)];
});
it(@"should layout height", ^{
[[theValue(height) should] equal:theValue(568.f)];
});
});
});
SPEC_END
我寫進去的寬高數值是iPhone6的寬高數值,如果用5S的模擬器來運行,將會返回5S的寬高 320 * 568
當我們 com+U 運行這段測試用例時。
控制臺的輸出
+ 'CalculateLayout, when calculate width and height, should layout width' [PASSED]
+ 'CalculateLayout, when calculate width and height, should layout height' [PASSED]
可以看到,由于有context
的存在,以及其可以嵌套的特性,測試的流程控制相比傳統測試可以更加精確。我們更容易把before
和after
的作用區域限制在合適的地方。
實際的測試寫在it里,是由一個一個的期望(Expectations)來進行描述的,期望相當于傳統測試中的斷言,要是運行的結果不能匹配期望,則測試失敗。在Kiwi
中期望都由should
或者shouldNot
開頭,并緊接一個或多個判斷的的鏈式調用,大部分常見的是be
或者haveSomeCondition
的形式。在我們上面的例子中我們使用了should not be nil
和should equal
兩個期望來確保字符串賦值的行為正確。其他的期望語句非常豐富,并且都符合自然語言描述,所以并不需要太多介紹。在使用的時候不妨直接按照自己的想法來描述自己的期望,一般情況下在IDE
的幫助下我們都能找到想要的結果。如果您想看看完整的期望語句的列表,可以參看文檔的這個頁面。從這一點來看,Kiwi可以說是一個非常靈活并具有可擴展性的測試框架。
來解釋下上面的語法中用到的theValue
.
Kiwi
為我們提供了一個標量轉對象的語法糖,叫做theValue
,在做精確比較的時候我們可以直接使用例子中直接與320.f或者568.f
做比較這樣的寫法來進行對比。
通過這樣一個簡單的例子,我們基本能掌握Kiwi的語法,以及Kiwi的使用。單元測試的門其實很好進,但是如何用心的,動腦子的去寫單元測試,則是對我們程序員莫大的考驗哦。
我講的并不完善,也不詳細,就算簡單記錄自己目前的收獲吧。