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

截圖測試

開發(fā)者對于為自己的應用寫測試有自己的動機。雖然我認為應該寫測試,但是這篇文章不是來勸說你來做這個的。

為一個 app 的表現(xiàn)層寫測試是一件棘手的工作。Apple 對于對象的邏輯測試已經有內建的支持,但是卻沒有支持測試那些界面代碼的結果。這個功能上的鴻溝實際上造成了很多開發(fā)者因為界面測試的復雜性而選擇忽視它。

當 Facebook 發(fā)布 FBSnapshotTestCaseCocoaPods 的時候,我起初還因為這個理由忽視了它, 還好我的同事沒有。

基于界面的測試意味著驗證你用戶最終看到的是不是你希望用戶看到的。測試界面可以保證不同版本,不同狀態(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 就可以安裝這個庫了。

帶截圖的 XCTest

默認的截圖測試需要繼承 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

缺點

沒有事情是完美的。讓我們談談不好的一面吧。

  • 測試異步的代碼很困難。這是在 Cocoa 的測試中經常出現(xiàn)的問題。我有兩個解決方案。你可以使用像 Specta 或者像 Kiwi 這樣的測試框架,它提供了多次運行斷言直到超時或者成功。這意味著你可以給它 0.5 秒的時間運行,同時測試可能被重復多次?;蛘?,你還可以在開發(fā)你的應用代碼的過程中,讓異步的代碼在做了標記的時候同步運行。
  • 有些組件測試起來很困難。有兩個需要注意的例子:在測試中一些 UIView 類不能在沒有 frame 的時候初始化,所以請總是給你的 view 一個 frame 來避免 <Error>: CGContextAddRect: invalid context 0x0. [..] 這樣的錯誤信息。 如果你使用了很多 Auto Layout 代碼,那么就不會那么簡單了?;?CATiledLayer 的 view 需要在 main screen 上并且在渲染瓦片 (tiles) 前被展現(xiàn)出來。它們同樣是異步渲染的。我一般為這些測試加入 兩秒等待。
  • 蘋果的操作系統(tǒng)補丁會改變部件的渲染方式。比如 Apple 在 iOS 7.1 悄悄改變了字體微調 (font hinting),所有使用到 UILabels 的截圖都需要重新錄制。
  • 每一個截圖是一個存在你倉庫里面的 PNG 文件,對我的情況來說,每個文件通常大小在 30-100kb 之間。我用 "@2x" 模式記錄了所有的測試。截圖隨著被記錄的 view 的增多而增長。

優(yōu)點

  • 我最終做到測試了。我通過對每個通過改變對象而得到的不同的 view 的狀態(tài)編寫了截圖測試。這讓我能夠讓在單次的測試中馬上看到不同狀態(tài)的變化。不用在我的 app 中進行點擊來進入對應的視圖,然后改變狀態(tài)。我只需要看看 FBSnapshotTestCase 渲染的視圖,這省去了很多開發(fā)時間。
  • 截圖測試在你運行其他測試的同時運行,不需要作為另外的測試 scheme 進行。它用和其他測試相同的語言書寫。它們大多數(shù)情況下可以在不將視圖顯示在屏幕上的時候運行。
  • 截圖可以讓代碼審查變得更具體。首先是測試,它們提供一種承諾,說明將要出現(xiàn)的變化。之后是截圖,證明測試的承諾是正確的。最后是代碼庫中的變更。在這個時候,你已經知道變化的內容了,你不僅對內在的改變清楚明白,也對用戶在外表上將看到的變化了然于胸。
  • 讓代碼審查可視化,可以讓設計師也參與其中。他們通過監(jiān)測項目的倉庫里面的圖片控制變化。
  • 我發(fā)現(xiàn)寫截圖測試可以提供更多的測試覆蓋度。我不相信 100% 的單元測試覆蓋度就是最優(yōu)方案。我盡量做實用的測試,測試大多數(shù)引入的變化。截圖測試在不用指定代碼路徑的時候就測試了很多代碼路徑。這是因為截圖測試可以很容易地測試一系列系統(tǒng)元素結合的結果。通過比較截圖可以方便的,你可以很快速地達到可觀的測試覆蓋率。
  • 截圖測試非??欤?Macbook Air 中使用 retina 的 iPad 尺寸的圖片平均每個測試需要運行 0.015 到 0.080 秒。一個應用里面運行上百個測試都沒問題。我在開發(fā)的應用 有數(shù)百個測試,但是它們能在 5 秒以內運行完畢。

工具

FBSnapShots + Specta + Expecta

我沒有使用原生的 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

Snapshots Xcode 插件

解析 console 里面的日志來找到圖片要花不少力氣,裝載不同的失敗測試到一個可視化的工具比如 Kaleidoscope 里,需要運行不少命令行程序。

為了處理幾乎所有這些常見的場景,我寫了一個 Xcode 插件 Snapshots。它可以通過 Alcatraz 安裝或者自己編譯。它可以讓在 Xcode 中失敗測試的失敗和成功的圖片的比較變得非常容易。

總結

FBSnapshotTestCase 給你一個測試視圖相關代碼的方法,它可以用來測試視圖相關的狀態(tài)而不用依賴于模擬器。如果你使用 Xcode 的話,你可以考慮和我的插件 Snapshots 一起使用它。有些時候它可能會讓人很煩,但是這還是值得的。它可以讓設計師參與代碼審查階段,也可以成為為現(xiàn)有項目寫測試的簡單的第一步,你可以試一試。

開源項目案例: