開發(fā)者對于為自己的應用寫測試有自己的動機。雖然我認為應該寫測試,但是這篇文章不是來勸說你來做這個的。
為一個 app 的表現(xiàn)層寫測試是一件棘手的工作。Apple 對于對象的邏輯測試已經有內建的支持,但是卻沒有支持測試那些界面代碼的結果。這個功能上的鴻溝實際上造成了很多開發(fā)者因為界面測試的復雜性而選擇忽視它。
當 Facebook 發(fā)布 FBSnapshotTestCase
到 CocoaPods 的時候,我起初還因為這個理由忽視了它, 還好我的同事沒有。
基于界面的測試意味著驗證你用戶最終看到的是不是你希望用戶看到的。測試界面可以保證不同版本,不同狀態(tài)的視圖看起來可以保持一致。界面測試可以用來提供一個高級別的測試,這涵蓋了很多相關對象的用例。
FBSnapShotTestCase
將一個 UIView
或者 CALayer
的子類渲染為一個 UIImage
。這個截圖被用和一個已經保存了的截圖進行比對,從而創(chuàng)建測試并生成測試的版本。當測試失敗的時候,將創(chuàng)建一個失敗的測試的參考圖片,并且創(chuàng)建一個另外的圖像來表現(xiàn)兩者的不同之處。
這是一個失敗的測試的例子,原因是我們的一個 View Controller 中 gird 元素比預期的要少:
http://wiki.jikexueyuan.com/project/objc/images/15-6.png" alt="" />
它通過將 view 或者 layer 以及已經存在的截圖渲染到兩個 CGContextRefs
,并且用 C 函數(shù) memcmp()
來進行內存比較。這樣的比較會非???,我在一臺 MacBook Air 上生成 iPad 或者 iPhone 的全屏截圖并進行測試,每張圖耗時在 0.013 到 0.086 秒之間。
當配置好以后,它默認會將參考圖片存儲到你項目的 [Project]Tests
目錄里面的一個叫 ReferenceImages
的子文件夾里。文件夾中是根據(jù)你的測試用例的類名建立的文件夾,在測試例文件夾中是每個測試的參考圖片。當一個測試失敗的時候,它會將失敗的結果存儲下來,另外再存儲一張這個結果和參考圖片的差異對比所生成圖片。三張圖片都會放到應用的 tmp 目錄下,截圖測試同時會用 NSLog
在控制臺輸出一條命令,你可以用這條命令來啟動 Kaleidoscope 并進行可視化的比較。
我們就不在這里兜圈子了:你應該在使用 CocoaPods 吧,所以安裝僅僅需要在你的 Podfile 的測試 target 里面加入 pod "FBSnapshotTestCase"
。運行 pod install
就可以安裝這個庫了。
默認的截圖測試需要繼承 FBSnapshotTestCase
而不是 XCTestCase
,然后使用 FBSnapshotVerifyView(viewOrLayer, "optional identifier")
宏來和已經存在的圖片驗證比較。這里的子類有一個 recordMode
的 boolean 屬性。當設置了這個值的時候,會錄制一個新的截圖而不是把結果和參考圖片做比較。
@interface ORSnapshotTestCase : FBSnapshotTestCase
@end
@implementation ORSnapshotTestCase
- (void)testHasARedSquare
{
// Removing this will verify instead of recording
self.recordMode = YES;
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 80, 80)];
view.backgroundColor = [UIColor redColor];
FBSnapshotVerifyView(view, nil);
}
@end
沒有事情是完美的。讓我們談談不好的一面吧。
UIView
類不能在沒有 frame 的時候初始化,所以請總是給你的 view 一個 frame 來避免 <Error>: CGContextAddRect: invalid context 0x0. [..]
這樣的錯誤信息。 如果你使用了很多 Auto Layout 代碼,那么就不會那么簡單了?;?CATiledLayer
的 view 需要在 main screen 上并且在渲染瓦片 (tiles) 前被展現(xiàn)出來。它們同樣是異步渲染的。我一般為這些測試加入 兩秒等待。UILabels
的截圖都需要重新錄制。FBSnapshotTestCase
渲染的視圖,這省去了很多開發(fā)時間。我沒有使用原生的 XCTest。我用的是 Specta 和 Expecta,因為使用的時候更加簡單,可讀性也更強。這是你在創(chuàng)建一個新 CocoaPod 的時候的初始配置。我是 Expecta+Snapshots 這個 pod 的貢獻者,它為 FBSnapshotTestCase
提供了一個類似 Expecta 的 API。它會為截圖命名,同時可以在視圖的生命周期里面選擇性運行。我的 Podfile 看起來是這樣子的:
target 'MyApp Tests', :exclusive => true do
pod 'Specta','~> 1.0'
pod 'Expecta', '~> 1.0'
pod 'Expecta+Snapshots', '~> 1.0'
end
然后,我的測試看起來會是這個樣子的:
SpecBegin(ORMusicViewController)
it (@"notations in black and white look correct", ^{
UIView *notationView = [[ORMusicNotationView alloc] initWithFrame:CGRectMake(0, 0, 80, 320)];
notationView.style = ORMusicNotationViewStyleBlackWhite;
expect(notationView).to.haveValidSnapshot();
});
it (@"Initial music view controller looks corrects", ^{
id contoller = [[ORMusicViewController alloc] initWithFrame:CGRectMake(0, 0, 80, 80)];
controller.view.frame = [UIScreen mainScreen].bounds;
expect(controller).to.haveValidSnapshot();
});
SpecEnd
解析 console 里面的日志來找到圖片要花不少力氣,裝載不同的失敗測試到一個可視化的工具比如 Kaleidoscope 里,需要運行不少命令行程序。
為了處理幾乎所有這些常見的場景,我寫了一個 Xcode 插件 Snapshots。它可以通過 Alcatraz 安裝或者自己編譯。它可以讓在 Xcode 中失敗測試的失敗和成功的圖片的比較變得非常容易。
FBSnapshotTestCase
給你一個測試視圖相關代碼的方法,它可以用來測試視圖相關的狀態(tài)而不用依賴于模擬器。如果你使用 Xcode 的話,你可以考慮和我的插件 Snapshots 一起使用它。有些時候它可能會讓人很煩,但是這還是值得的。它可以讓設計師參與代碼審查階段,也可以成為為現(xiàn)有項目寫測試的簡單的第一步,你可以試一試。
開源項目案例: