iOS開發之TDD和BDD

定義

是一種使用自動化單元測試來推動軟件設計並強制依賴關係解耦的技術。使用這種做法的結果是一套全面的單元測試,可隨時運行,以提供軟件可以正常工作的反饋。

TDD重點是培養整個研發過程的節奏感,就像跳踢踏舞一樣,“ti-ta-ti”。

在編寫真正實現功能的代碼之前先編寫測試,每次測試之後,重構完成,然後再次執行相同或類似的測試。該過程根據需要重複多次,直到每個單元根據所需的規格運行。

行為驅動開發(Behavior-driven development)是一種敏捷軟件開發的技術,BDD的重點是通過與利益相關者的討論取得對預期的軟件行為的清醒認識。它通過用自然語言書寫非程序員可讀的測試用例擴展了測試驅動開發方法。

測試驅動開發(Test-driven development)是軟件開發過程中的應用方法,由極限編程中倡導,以其倡導先寫測試程序,然後編碼實現其功能得名。測試驅動開發是戴兩頂帽子思考的開發方式:先戴上實現功能的帽子,在測試的輔助下,快速實現其功能;再戴上重構的帽子,在測試的保護下,通過去除冗餘的代碼,提高代碼質量。測試驅動著整個開發過程:首先,驅動代碼的設計和功能的實現;其後,驅動代碼的再設計和重構。

iOS中非常有名並且好用的BDD框架 —— Kiwi。

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

行為驅動開發(BDD)正是為了解決上述問題而生的,作為第二代敏捷方法,BDD提倡的是通過將測試語句轉換為類似自然語言的描述,開發人員可以使用更符合大眾語言的習慣來書寫測試,這樣不論在項目交接/交付,或者之後自己修改時,都可以順利很多

如果說作為開發者的我們日常工作是寫代碼,那麼BDD其實就是在講故事。

一個典型的BDD的測試用例包活完整的三段式上下文,測試大多可以翻譯為Given..When..Then的格式,讀起來輕鬆愜意。BDD在其他語言中也已經有一些框架,包括最早的Java的JBehave和赫赫有名的Ruby的RSpec和Cucumber。

在objc中,現在比較流行的BDD框架有cedar,specta和Kiwi。

區別

首先,TDD基本上是單獨提的,目前不存在一個跟它同等層級的實踐,Industrial Logic有在提倡Micro Test,但那也是跟Unit Test對應更低一級,跟TDD對應的,其實沒有別的實踐。TDD指的是在單元測試級別,也即函數級別進行測試驅動開發。

BDD,不是跟TDD一個層級的,B是說代碼的行為,或許比單元測試高那麼一點點吧,主要是跟ATDD(接收測試驅動開發)、SBE(實例化需求)等實踐一併提及的,因為他們都是對應到傳統測試理論裡面,高於單元和模塊測試,從功能測試、集成到系統、性能等這些高級別測試的範圍。

所以說,TDD、BDD根本不是一個層面的東西,解決的是不同的問題。但BDD、ATDD、SBE基本上都認為TDD是基礎,也即是說,他們主張做BDD、ATDD、SBE必做TDD。但反之則未必。

另一方面,TDD通常使用的是代碼層級測試的工具,最常見的是xUnit家族,單元測試的寫法沒有特別的侷限,無非就是調用函數,檢查返回。

BDD,有非常有辨識力的行為測試用例格式,也即GWT結構,但這個可以認為是語法,只要工具可以識別這個語法和執行即可,目前沒有特別印象單元測試級別的工具可以識別這個語法。主要還是相應層級的工具,最為知名的是Cucumber。

BDD的好處

軟件開發過程中最常見的兩個問題

需求和開發脫節:

  • 用戶想要的功能沒有開發
  • 開發的功能並非用戶想要
  • 用戶和開發人員所說語言不同

開發和測試脫節:

  • 開發和測試被認為割裂
  • 從開發到測試周期過長
  • 測試自動化程度低

3. 如何解決上面說的兩個問題

使用BDD可以解決需求和開發脫節的問題,首先他們都是從用戶的需求出發,保證程序實現效果與用戶需求一致。

很多人都認為BDD跟TDD很接近,跟TDD相比,BDD就是基於我們寫的需求行為和文檔來驅動開發,這些文檔描述跟我們的測試代碼很相像。如:

var assert = require('assert'),
factorial = require('../index');
describe('Test', function (){
before(function(){
// Stuff to do before the tests, like imports, what not
});
describe('#factorial()', function (){
it('should return 1 when given 0', function (){
factorial(0).should.equal(1);
});
it('should return 1 when given 1', function (){
factorial(1).should.equal(1);
});
it('should return 2 when given 2', function (){
factorial(2).should.equal(2);
});
it('should return 6 when given 3', function (){
factorial(3).should.equal(6);
});
});
after(function () {

// Anything after the tests have finished
});
});

主要區別就是在於測試的描述上,BDD使用一種更通俗易懂的文字來描述測試用例。儘管上面的例子很簡單,但從中我們可以看出BDD更關注需求的功能,而不是實際結果,BDD相對於TDD而言更有利於幫助我們設計軟件開發。

BDD具體使用,以Kiwi 為例

在自己的項目中 加入Kiwi:

pod 'Kiwi'

github上的例子:

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

很容易根據上下文將其提取為Given..When..Then的三段式自然語言。

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 實例

如:iOS項目代碼:

//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;
}

測試用例:

#import <kiwi>
#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
/<kiwi>

輸出:

+ 'CalculateLayout, when calculate width and height, should layout width' [PASSED]
+ 'CalculateLayout, when calculate width and height, should layout height' [PASSED]
iOS開發之TDD和BDD


分享到:


相關文章: