開(kāi)發(fā)者對(duì)于為自己的應(yīng)用寫測(cè)試有自己的動(dòng)機(jī)。雖然我認(rèn)為應(yīng)該寫測(cè)試,但是這篇文章不是來(lái)勸說(shuō)你來(lái)做這個(gè)的。
為一個(gè) app 的表現(xiàn)層寫測(cè)試是一件棘手的工作。Apple 對(duì)于對(duì)象的邏輯測(cè)試已經(jīng)有內(nèi)建的支持,但是卻沒(méi)有支持測(cè)試那些界面代碼的結(jié)果。這個(gè)功能上的鴻溝實(shí)際上造成了很多開(kāi)發(fā)者因?yàn)榻缑鏈y(cè)試的復(fù)雜性而選擇忽視它。
當(dāng) Facebook 發(fā)布 FBSnapshotTestCase 到 CocoaPods 的時(shí)候,我起初還因?yàn)檫@個(gè)理由忽視了它, 還好我的同事沒(méi)有。
基于界面的測(cè)試意味著驗(yàn)證你用戶最終看到的是不是你希望用戶看到的。測(cè)試界面可以保證不同版本,不同狀態(tài)的視圖看起來(lái)可以保持一致。界面測(cè)試可以用來(lái)提供一個(gè)高級(jí)別的測(cè)試,這涵蓋了很多相關(guān)對(duì)象的用例。
FBSnapShotTestCase 將一個(gè) UIView 或者 CALayer 的子類渲染為一個(gè) UIImage。這個(gè)截圖被用和一個(gè)已經(jīng)保存了的截圖進(jìn)行比對(duì),從而創(chuàng)建測(cè)試并生成測(cè)試的版本。當(dāng)測(cè)試失敗的時(shí)候,將創(chuàng)建一個(gè)失敗的測(cè)試的參考圖片,并且創(chuàng)建一個(gè)另外的圖像來(lái)表現(xiàn)兩者的不同之處。
這是一個(gè)失敗的測(cè)試的例子,原因是我們的一個(gè) View Controller 中 gird 元素比預(yù)期的要少:
http://wiki.jikexueyuan.com/project/objc/images/15-6.png" alt="" />
它通過(guò)將 view 或者 layer 以及已經(jīng)存在的截圖渲染到兩個(gè) CGContextRefs,并且用 C 函數(shù) memcmp() 來(lái)進(jìn)行內(nèi)存比較。這樣的比較會(huì)非???,我在一臺(tái) MacBook Air 上生成 iPad 或者 iPhone 的全屏截圖并進(jìn)行測(cè)試,每張圖耗時(shí)在 0.013 到 0.086 秒之間。
當(dāng)配置好以后,它默認(rèn)會(huì)將參考圖片存儲(chǔ)到你項(xiàng)目的 [Project]Tests 目錄里面的一個(gè)叫 ReferenceImages 的子文件夾里。文件夾中是根據(jù)你的測(cè)試用例的類名建立的文件夾,在測(cè)試?yán)募A中是每個(gè)測(cè)試的參考圖片。當(dāng)一個(gè)測(cè)試失敗的時(shí)候,它會(huì)將失敗的結(jié)果存儲(chǔ)下來(lái),另外再存儲(chǔ)一張這個(gè)結(jié)果和參考圖片的差異對(duì)比所生成圖片。三張圖片都會(huì)放到應(yīng)用的 tmp 目錄下,截圖測(cè)試同時(shí)會(huì)用 NSLog 在控制臺(tái)輸出一條命令,你可以用這條命令來(lái)啟動(dòng) Kaleidoscope 并進(jìn)行可視化的比較。
我們就不在這里兜圈子了:你應(yīng)該在使用 CocoaPods 吧,所以安裝僅僅需要在你的 Podfile 的測(cè)試 target 里面加入 pod "FBSnapshotTestCase"。運(yùn)行 pod install 就可以安裝這個(gè)庫(kù)了。
默認(rèn)的截圖測(cè)試需要繼承 FBSnapshotTestCase 而不是 XCTestCase,然后使用 FBSnapshotVerifyView(viewOrLayer, "optional identifier") 宏來(lái)和已經(jīng)存在的圖片驗(yàn)證比較。這里的子類有一個(gè) recordMode 的 boolean 屬性。當(dāng)設(shè)置了這個(gè)值的時(shí)候,會(huì)錄制一個(gè)新的截圖而不是把結(jié)果和參考圖片做比較。
@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
沒(méi)有事情是完美的。讓我們談?wù)劜缓玫囊幻姘伞?/p>
UIView 類不能在沒(méi)有 frame 的時(shí)候初始化,所以請(qǐng)總是給你的 view 一個(gè) frame 來(lái)避免 <Error>: CGContextAddRect: invalid context 0x0. [..] 這樣的錯(cuò)誤信息。 如果你使用了很多 Auto Layout 代碼,那么就不會(huì)那么簡(jiǎn)單了?;?CATiledLayer 的 view 需要在 main screen 上并且在渲染瓦片 (tiles) 前被展現(xiàn)出來(lái)。它們同樣是異步渲染的。我一般為這些測(cè)試加入 兩秒等待。UILabels 的截圖都需要重新錄制。FBSnapshotTestCase 渲染的視圖,這省去了很多開(kāi)發(fā)時(shí)間。我沒(méi)有使用原生的 XCTest。我用的是 Specta 和 Expecta,因?yàn)槭褂玫臅r(shí)候更加簡(jiǎn)單,可讀性也更強(qiáng)。這是你在創(chuàng)建一個(gè)新 CocoaPod 的時(shí)候的初始配置。我是 Expecta+Snapshots 這個(gè) pod 的貢獻(xiàn)者,它為 FBSnapshotTestCase 提供了一個(gè)類似 Expecta 的 API。它會(huì)為截圖命名,同時(shí)可以在視圖的生命周期里面選擇性運(yùn)行。我的 Podfile 看起來(lái)是這樣子的:
target 'MyApp Tests', :exclusive => true do
pod 'Specta','~> 1.0'
pod 'Expecta', '~> 1.0'
pod 'Expecta+Snapshots', '~> 1.0'
end
然后,我的測(cè)試看起來(lái)會(huì)是這個(gè)樣子的:
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 里面的日志來(lái)找到圖片要花不少力氣,裝載不同的失敗測(cè)試到一個(gè)可視化的工具比如 Kaleidoscope 里,需要運(yùn)行不少命令行程序。
為了處理幾乎所有這些常見(jiàn)的場(chǎng)景,我寫了一個(gè) Xcode 插件 Snapshots。它可以通過(guò) Alcatraz 安裝或者自己編譯。它可以讓在 Xcode 中失敗測(cè)試的失敗和成功的圖片的比較變得非常容易。
FBSnapshotTestCase 給你一個(gè)測(cè)試視圖相關(guān)代碼的方法,它可以用來(lái)測(cè)試視圖相關(guān)的狀態(tài)而不用依賴于模擬器。如果你使用 Xcode 的話,你可以考慮和我的插件 Snapshots 一起使用它。有些時(shí)候它可能會(huì)讓人很煩,但是這還是值得的。它可以讓設(shè)計(jì)師參與代碼審查階段,也可以成為為現(xiàn)有項(xiàng)目寫測(cè)試的簡(jiǎn)單的第一步,你可以試一試。
開(kāi)源項(xiàng)目案例: