鍍金池/ 教程/ iOS/ 測試 View Controllers
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

測試 View Controllers

我們不是迷信測試,但它應(yīng)該幫助我們加快開發(fā)進(jìn)度,并且讓事情變得更有趣。

讓事情保持簡單

測試簡單的事情很簡單,同樣,測試復(fù)雜的事會很復(fù)雜。就像我們在其他文章中指出的那樣,讓事情保持簡單小巧總是好的。除此之外,它還有利于我們測試。這是件雙贏的事。讓我們來看看測試驅(qū)動開發(fā)(簡稱 TDD),有些人喜歡它,有些人則不喜歡。我們在這里不深入討論,只是如果用 TDD,你得在寫代碼之前先寫好測試。如果你好奇的話,可以去找 Wikipedia 上的文章看看。同時,我們也認(rèn)為重構(gòu)和測試可以很好地結(jié)合在一起。

測試 UI 部分通常很麻煩,因為它們包含太多活動部件。通常,view controller 需要和大量的 model 和 view 類交互。為了使 view controller 便于測試,我們要讓任務(wù)盡量分離。

幸好,我們在更輕量的 view controller 這篇文章中的闡述的技術(shù)可以讓測試更加簡單。通常,如果你發(fā)現(xiàn)有些地方很難做測試,這就說明你的設(shè)計出了問題,你應(yīng)該重構(gòu)它。你可以重新參考更輕量的 view controller 這篇文章來獲得一些幫助??偟哪繕?biāo)就是有清晰的關(guān)注點分離。每個類只做一件事,并且做好。這樣就可以讓你只測試這件事。

記住:測試越多,回報的增長趨勢越慢。首先你應(yīng)該做簡單的測試。當(dāng)你覺得滿意時,再加入更多復(fù)雜的測試。

Mocking

當(dāng)你把一個整體拆分成小零件(比如更小的類)時,我們可以針對每個小的類來進(jìn)行測試。但由于我們測試的類會和其他類交互,這里我們用一個所謂的 mockstub 來繞開它。把 mock 對象看成是一個占位符,我們測試的類會跟這個占位符交互,而不是真正的那個對象。這樣,我們就可以針對性地測試,并且保證不依賴于應(yīng)用程序的其他部分。

在示例程序中,我們有個包含數(shù)組的 data source 需要測試。這個 data source 會在某個時候從 table view 中取出(dequeue)一個 cell。在測試過程中,還沒有 table view,但是我們傳遞一個 mock 的 table view,這樣即使沒有 table view,也可以測試 data source,就像下面你即將看到的。起初可能有點難以理解,多看幾次后,你就能體會到它的強(qiáng)大和簡單。

Objective-C 中有個用來 mocking 的強(qiáng)大工具叫做 OCMock。它是一個非常成熟的項目,充分利用了 Objective-C 運行時強(qiáng)大的能力和靈活性。它使用了一些很酷的技巧,讓通過 mock 對象來測試變得更加有趣。

本文后面有 data source 測試的例子,它更加詳細(xì)地展示了這些技術(shù)如何工作在一起。

SenTestKit

編者注 這一節(jié)有一些過時了。在 Xcode 5 中 SenTestingKit 已經(jīng)被 XCTest 完全取代,不過兩者使用上沒有太多區(qū)別,我們可以通過 Xcode 的 Edit -> Refactor -> Convert to XCTest 選項來切換到新的測試框架

我們將要使用的另一個工具是一個測試框架,開發(fā)者工具的一部分:Sente 的 SenTestingKit。這個上古神器從 1997 年起就伴隨在 Objective-C 開發(fā)者左右,比第一款 iPhone 發(fā)布還早 10 年。現(xiàn)在,它已經(jīng)集成到 Xcode 中了。SenTestingKit 會運行你的測試。通過 SenTestingKit,你將測試組織在類中。你需要給每一個你想測試的類創(chuàng)建一個測試類,類名以 Tests 結(jié)尾,它反應(yīng)了這個類是干什么的。

這些測試類里的方法會做具體的測試工作。方法名必須以 test 開頭來作為觸發(fā)一個測試運行的條件。還有特殊的 -setUp-tearDown 方法,你可以重載它們來設(shè)置各個測試。記住,你的測試類就是個類而已:只要對你有幫助,可以按需求在里面加 properties 和輔助方法。

做測試時,為測試類創(chuàng)建基類是個不錯的模式。把通用的邏輯放到基類里面,可以讓測試更簡單和集中??梢酝ㄟ^示例程序中的例子來看看這樣帶來的好處。我們沒有使用 Xcode 的測試模板,為了讓事情簡單有效,我們只創(chuàng)建了單獨的 .m 文件。通過把類名改成以 Tests 結(jié)尾,類名可以反映出我們在對什么做測試。

編者注 Xcode 5 中 默認(rèn)的測試模板也不再會自動創(chuàng)建 .h 文件了

與 Xcode 集成

測試會被 build 成一個 bundle,其中包含一個動態(tài)庫和你選擇的資源文件。如果你要測試某些資源文件,你得把它們加到測試的 target 中,Xcode 就會將它們打包到一個 bundle 中。接著你可以通過 NSBundle 來定位這些資源文件,示例項目實現(xiàn)了一個 -URLForResource:withExtension: 方法來方便的使用它。

Xcode 中的每個 scheme 定義了相應(yīng)的測試 bundle 是哪個。通過 ?-R 運行程序,?-U 運行測試。

測試的運行依附于程序的運行,當(dāng)程序運行時,測試 bundle 將被注入(injected)。測試時,你可能不想讓你的程序做太多的事,那樣會對測試造成干擾。可以把下面的代碼加到 app delegate 中:

static BOOL isRunningTests(void) __attribute__((const));

- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    if (isRunningTests()) {
        return YES;
    }

    //
    // Normal logic goes here
    //

    return YES;
}

static BOOL isRunningTests(void)
{
    NSDictionary* environment = [[NSProcessInfo processInfo] environment];
    NSString* injectBundle = environment[@"XCInjectBundle"];
    return [[injectBundle pathExtension] isEqualToString:@"octest"];
}

編輯 Scheme 給了你極大的靈活性。你可以在測試之前或之后運行腳本,也可以有多個測試 bundle。這對大型項目來說很有用。最重要的是,可以打開或關(guān)閉個別測試,這對調(diào)試測試非常有用,只是要記得之后再把它們重新全部打開。

還要記住你可以為測試代碼下斷點,當(dāng)測試執(zhí)行時,調(diào)試器會在斷點處停下來。

測試 Data Source

好了,讓我們開始吧。我們已經(jīng)通過拆分 view controller 讓測試工作變得更輕松了?,F(xiàn)在我們要測試 ArrayDataSource。首先我們新建一個空的,基本的測試類。我們把接口和實現(xiàn)都放到一個文件里;也沒有哪個地方需要包含 @interface,放到一個文件會顯得更加漂亮和整潔。

#import "PhotoDataTestCase.h"

@interface ArrayDataSourceTest : PhotoDataTestCase
@end

@implementation ArrayDataSourceTest
- (void)testNothing;
{
    STAssertTrue(YES, @"");
}
@end

這個類沒做什么事,只是展示了基本的設(shè)置。當(dāng)我們運行這個測試時,-testNothing 方法將會運行。特別地,STAssert 宏將會做瑣碎的檢查。注意,前綴 ST 源自于 SenTestingKit。這些宏和 Xcode 集成,會把失敗顯示到側(cè)邊面板的 Issues 導(dǎo)航欄中。

第一個測試

我們現(xiàn)在把 testNothing 替換成一個簡單、真正的測試:

- (void)testInitializing;
{
    STAssertNil([[ArrayDataSource alloc] init], @"Should not be allowed.");
    TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){};
    id obj1 = [[ArrayDataSource alloc] initWithItems:@[]
                                      cellIdentifier:@"foo"
                                  configureCellBlock:block];
    STAssertNotNil(obj1, @"");
}

實踐 Mocking

接著,我們想測試 ArrayDataSource 實現(xiàn)的方法:

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath;

為此,我們創(chuàng)建一個測試方法:

- (void)testCellConfiguration;

首先,創(chuàng)建一個 data source:

__block UITableViewCell *configuredCell = nil;
__block id configuredObject = nil;
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){
    configuredCell = a;
    configuredObject = b;
};
ArrayDataSource *dataSource = [[ArrayDataSource alloc] initWithItems:@[@"a", @"b"]
                                                      cellIdentifier:@"foo"
                                                  configureCellBlock:block];

注意,configureCellBlock 除了存儲對象以外什么都沒做,這可以讓我們可以更簡單地測試它。

然后,我們?yōu)?table view 創(chuàng)建一個 mock 對象

id mockTableView = [OCMockObject mockForClass:[UITableView class]];

Data source 將在傳進(jìn)來的 table view 上調(diào)用 -dequeueReusableCellWithIdentifier:forIndexPath: 方法。我們將告訴 mock object 當(dāng)它收到這個消息時要做什么。首先創(chuàng)建一個 cell,然后設(shè)置 mock。

UITableViewCell *cell = [[UITableViewCell alloc] init];
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[[[mockTableView expect] andReturn:cell]
        dequeueReusableCellWithIdentifier:@"foo"
                             forIndexPath:indexPath];

第一次看到它可能會覺得有點迷惑。我們在這里所做的,是讓 mock 記錄特定的調(diào)用。Mock 不是一個真正的 table view;我們只是假裝它是。-expect 方法允許我們設(shè)置一個 mock,讓它知道當(dāng)這個方法調(diào)用時要做什么。

另外,-expect 方法也告訴 mock 這個調(diào)用必須發(fā)生。當(dāng)我們稍后在 mock 上調(diào)用 -verify 時,如果那個方法沒有被調(diào)用過,測試就會失敗。相應(yīng)地,-stub 方法也用來設(shè)置 mock 對象,但它不關(guān)心方法是否被調(diào)用過。

現(xiàn)在,我們要觸發(fā)代碼運行。我們就調(diào)用我們希望測試的方法。

NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
id result = [dataSource tableView:mockTableView
            cellForRowAtIndexPath:indexPath];

然后我們測試是否一切正常:

STAssertEquals(result, cell, @"Should return the dummy cell.");
STAssertEquals(configuredCell, cell, @"This should have been passed to the block.");
STAssertEqualObjects(configuredObject, @"a", @"This should have been passed to the block.");
[mockTableView verify];

STAssert 宏測試值的相等性。注意,前兩個測試,我們通過比較指針來完成;我們不使用 -isEqual:,是因為我們實際希望測試的是 resultcellconfiguredCell 都是同一個對象。第三個測試要用 -isEqual:,最后我們調(diào)用 mock 的 -verify 方法。

注意,在示例程序中,我們是這樣設(shè)置 mock 的:

id mockTableView = [self autoVerifiedMockForClass:[UITableView class]];

這是我們測試基類中的一個方便的封裝,它會在測試最后自動調(diào)用 -verify 方法。

測試 UITableViewController

下面,我們轉(zhuǎn)向 PhotosViewController。它是個 UITableViewController 的子類,它使用了我們剛才測試過的 data source。View controller 剩下的代碼已經(jīng)相當(dāng)簡單了。

我們想測試點擊 cell 后把我們帶到詳情頁面,即一個 PhotoViewController 的實例被 push 到 navigation controller 里面。我們再次使用 mocking 來讓測試盡可能不依賴于其他部分。

首先我們創(chuàng)建一個 UINavigationController 的 mock:

id mockNavController = [OCMockObject mockForClass:[UINavigationController class]];

接下來,我們要使用部分 mocking。我們希望 PhotosViewController 實例的 navigationController 返回 mockNavController。我們不能直接設(shè)置 navigation controller,所以我們簡單地用 stub 來替換掉 PhotosViewController 實例這個方法,讓它返回 mockNavController 就可以了。

PhotosViewController *photosViewController = [[PhotosViewController alloc] init];
id photosViewControllerMock = [OCMockObject partialMockForObject:photosViewController];
[[[photosViewControllerMock stub] andReturn:mockNavController] navigationController];

現(xiàn)在,任何時候?qū)?photosViewController 調(diào)用 -navigationController 方法,都會返回 mockNavController。這是個強(qiáng)大的技巧,OCMock 就有這樣的本領(lǐng)。

接下來,我們要告訴 navigation controller mock 我們調(diào)用的期望,即,一個 photo 不為 nil 的 detail view controller。

UIViewController* viewController = [OCMArg checkWithBlock:^BOOL(id obj) {
    PhotoViewController *vc = obj;
    return ([vc isKindOfClass:[PhotoViewController class]] &&
            (vc.photo != nil));
}];
[[mockNavController expect] pushViewController:viewController animated:YES];

現(xiàn)在,我們觸發(fā) view 加載,并且模擬一行被點擊:

UIView *view = photosViewController.view;
STAssertNotNil(view, @"");
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[photosViewController tableView:photosViewController.tableView
        didSelectRowAtIndexPath:indexPath];

最后我們驗證 mocks 上期望的方法被調(diào)用過:

[mockNavController verify];
[photosViewControllerMock verify];

現(xiàn)在我們有了一個測試,用來測試和 navigation controller 的交互,以及正確 view controller 的創(chuàng)建。

又一次地,我們在示例程序中使用了便捷的方法:

- (id)autoVerifiedMockForClass:(Class)aClass;
- (id)autoVerifiedPartialMockForObject:(id)object;

于是,我們不需要記住調(diào)用 -verify。

進(jìn)一步探索

就像你從上面看到的那樣,部分 mocking 非常強(qiáng)大。如果你看看 -[PhotosViewController setupTableView] 方法的源碼,你就會看到它是如何從 app delegate 中取出 model 對象的。

NSArray *photos = [AppDelegate sharedDelegate].store.sortedPhotos;

上面的測試依賴于這行代碼。打破這種依賴的一種方式是再次使用 部分 mocking,讓 app delegate 返回預(yù)定義的數(shù)據(jù),就像這樣:

id storeMock; // 假設(shè)我們已經(jīng)設(shè)置過了
id appDelegate = [AppDelegate sharedDelegate]
id appDelegateMock = [OCMockObject partialMockForObject:appDelegate];
[[[appDelegateMock stub] andReturn:storeMock] store];

現(xiàn)在,無論何時調(diào)用 [AppDelegate sharedDelegate].store ,它將返回 storeMock。將這個技術(shù)使用好的話,可以確保讓你的測試恰到好處地在保持簡單和應(yīng)對復(fù)雜之間找到平衡。

需要記住的事

部分 mock 技術(shù)將會在 mocks 的存在期間替換并保持被 mocking 的對象,并且一直有效。你可以通過提前調(diào)用 [aMock stopMocking] 來終于這種行為。大多數(shù)時候,你希望 部分 mock 在整個測試期間都保持有效。如果要提前終止,請確保在測試方法最后放置 [aMock verify]。否則 ARC 會過早釋放這個 mock,這樣你就不能 -verify 了,這不太可能是你想要的結(jié)果。

測試 NIB 加載

PhotoCell 設(shè)置在一個 NIB 中,我們可以寫一個簡單的測試來檢查 outlets 設(shè)置得是否正確。我們來回顧一下 PhotoCell 類:

@interface PhotoCell : UITableViewCell

+ (UINib *)nib;

@property (weak, nonatomic) IBOutlet UILabel* photoTitleLabel;
@property (weak, nonatomic) IBOutlet UILabel* photoDateLabel;

@end

我們的簡單測試的實現(xiàn)看上去是這樣:

@implementation PhotoCellTests

- (void)testNibLoading;
{
    UINib *nib = [PhotoCell nib];
    STAssertNotNil(nib, @"");

    NSArray *a = [nib instantiateWithOwner:nil options:@{}];
    STAssertEquals([a count], (NSUInteger) 1, @"");
    PhotoCell *cell = a[0];
    STAssertTrue([cell isMemberOfClass:[PhotoCell class]], @"");

    // 檢查 outlet 是否正確設(shè)置
    STAssertNotNil(cell.photoTitleLabel, @"");
    STAssertNotNil(cell.photoDateLabel, @"");
}

@end

非?;A(chǔ),但是能出色完成工作。

值得一提的是,當(dāng)有發(fā)生改變時,我們需要同時更新測試以及相應(yīng)的類或 nib 。這是事實。你需要考慮改變類或者 nib 文件時可能會打破原有的 outlets 連接。如果你用了 .xib 文件,你可能要注意了,這是經(jīng)常發(fā)生的事。

關(guān)于 Class 和 Injection

我們已經(jīng)從與 Xcode 集成得知,測試 bundle 會注入到應(yīng)用程序中。省略注入的如何工作的細(xì)節(jié)(它本身是個巨大的話題),簡單地說:注入是把待注入的 bundle(我們的測試 bundle)中的 Objective-C 類添加到運行的應(yīng)用程序中。這很好,因為這樣允許我們運行測試了。

還有一件事會很讓人迷惑,那就是如果我們同時把一個類添加到應(yīng)用程序和測試 bundle中。如果在上面的示例程序中,我們(不小心)把 PhotoCell 類同時添加到測試 bundle 和應(yīng)用程序里的話,在測試 bundle 中調(diào)用 [PhotoCell class] 會返回一個不同的指針(你應(yīng)用程序中的那個類)。于是我們的測試將會失?。?/p>

STAssertTrue([cell isMemberOfClass:[PhotoCell class]], @"");

再一次聲明:注入很復(fù)雜。你應(yīng)該確認(rèn)的是:不要把應(yīng)用程序中的 .m 文件添加到測試 target 中。否則你會得到預(yù)想不到的行為。

額外的思考

如果你使用一個持續(xù)集成 (CI) 的解決方案,讓你的測試啟動和運行是一個好主意。詳細(xì)的描述超過了本文的范圍。這些腳本通過 RunUnitTests 腳本觸發(fā)。還有個 TEST_AFTER_BUILD 環(huán)境變量。

另一種有趣的選擇是創(chuàng)建單獨的測試 bundle 來自動化性能測試。你可以在測試方法里做任何你想做的。定時調(diào)用一些方法并使用 STAssert 來檢查它們是否在特定閾值里面是其中一種選擇。

擴(kuò)展閱讀

上一篇:測試并發(fā)程序下一篇:iOS 7