鍍金池/ 教程/ iOS/ UI 測試
與四軸無人機(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ù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
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)試
項(xiàng)目介紹
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è)計(jì)優(yōu)雅的移動游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動畫
常見的后臺實(shí)踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實(shí)戰(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 過程

UI 測試

如何進(jìn)行 UI 測試是 iOS 開發(fā)中很常見的問題 (我猜測 Mac 等其他 UI 驅(qū)動的平臺也是這樣)。很多人完全不做 UI 測試,問起來他們經(jīng)常這樣說:“你只應(yīng)該測試你的業(yè)務(wù)邏輯?!?也有一部分人想做 UI 測試,但是覺得它太復(fù)雜于是便放棄了。

每當(dāng)有人和我說 UI 測試很難的時(shí)候,我就會回想起在一次測試小組討論中,Landon Fuller 談到 Paper (by 53) 項(xiàng)目的 UI 測試時(shí)說的一段話:

你在屏幕上看到的是各種數(shù)據(jù)和變化綜合之后按照時(shí)間變化所得到的結(jié)果。如果你可以將這些東西分解成可供測試的單元的話,就意味著你可以將相對復(fù)雜的內(nèi)容拆解成更容易理解的元素。

Paper 的 UI 相對來說算是復(fù)雜的了,當(dāng)構(gòu)建這樣的 UI 的時(shí)候,可測試性一般不會被考慮在內(nèi)。但是,用戶的任何一個(gè)行為在代碼中都是被建模處理的,在測試中模仿用戶的行為是一件很容易的事情。而問題在于大多數(shù)框架,包括 UIKit,都沒有公開的暴露測試所需要的底層結(jié)構(gòu)。

知道 “測試什么” 和知道 “如何測試” 同等重要。我一直都在提及 “UI 測試”,因?yàn)檫@是一個(gè)被廣為接受的概念,我即將深入討論這類測試。實(shí)際上,我覺得你可以把 UI 測試分成兩類:1) 行為 和 2) 外觀.

我們無法確定地說某種 UI 的外觀是正確的,因?yàn)?UI 的外觀總是在頻繁的變化著。你肯定不想每次修改 UI 的時(shí)候都去修改 UI 的測試。但這并不意味著你無法測試外觀。我對于這個(gè)方面沒有任何經(jīng)驗(yàn),但是我們可以用截屏的方式檢驗(yàn)外觀。如果想進(jìn)一步的了解,可以閱讀 Orta 關(guān)于這方面的文章。

在開始之前友情提示各位,這篇文章將會探討用戶行為測試相關(guān)的內(nèi)容。我在Github上提供了一個(gè)項(xiàng)目,里面包含了一些實(shí)際的例子,雖然是使用Objective-C編寫的iOS項(xiàng)目,但是背后的原理是可以應(yīng)用于Mac和其他UI框架的。

在我測試用戶行為時(shí)的第一條原則是:使用代碼的形式來模擬事件觸發(fā),并讓它們就好像真的是由用戶的行所觸發(fā)的那樣。這可能會有點(diǎn)困難,因?yàn)檎缜懊嫠f,并不是所有的框架都會公開底層接口。

類似于 KIF、FrankCalabash 的項(xiàng)目解決了這個(gè)問題,但是代價(jià)就是需要插入一個(gè)層額外的復(fù)雜度,而我們應(yīng)當(dāng)始終使用最簡單的可行方案。一般來說都測試的結(jié)果應(yīng)該是確定的,不修改的話要么就持續(xù)地失敗,要么就持續(xù)地成功。最糟糕的測試套件就是那些會隨機(jī)失敗的測試。我不會選擇去用那樣的方案,因?yàn)閺奈业慕?jīng)驗(yàn)來看,它們犧牲了可靠性和穩(wěn)定性而讓項(xiàng)目變得錯綜復(fù)雜。

注意到在示例項(xiàng)目中我使用了 SpectaExpecta,嚴(yán)格來講這并不是最簡單的解決方案,最簡單的解決方案是 XCText。但是又有很多原因讓我不得不提及它們。并且從我自己的開發(fā)經(jīng)驗(yàn)來看,它們并不會影響測試的可靠性和穩(wěn)定性。事實(shí)上,我敢打賭它們讓我的測試更好 (這是個(gè)安全的賭局,因?yàn)?strong>好是個(gè)模糊的概念的^_^)。

不管測試方法是什么,當(dāng)測試用戶行為的時(shí)候,我們總是想盡可能接近于用戶的真實(shí)操作。當(dāng)用戶與應(yīng)用交互的時(shí)候,我們往往希望能夠用代碼重現(xiàn)出來。想象一下,當(dāng)用戶看著一個(gè) ViewController,然后點(diǎn)擊了一個(gè)按鈕,彈出了一個(gè)新的 ViewController。你應(yīng)該是希望測試可以展示原始的 ViewConnector,并且實(shí)現(xiàn)點(diǎn)擊按鈕操作,然后確保呈現(xiàn)一個(gè)新的 ViewController。

專注于用代碼來模擬用戶交互,你可以一次驗(yàn)證多件事情。最重要的,你可以驗(yàn)證核實(shí)期望的行為。作為附贈,你也同時(shí)測試了控件正確被初始化以及它們的 action 是被正確設(shè)置的。

舉個(gè)例子,比如在某個(gè)測試中,我們直接調(diào)用了一個(gè)行為方法。這并不需要把你的測試和按鈕要做的事情連接起來,當(dāng)然實(shí)際上這樣的測試也不會去做這件事。但是如果按鈕的 target 或者 action 改變了,你的測試依舊可以通過。你希望證實(shí)的其實(shí)是按鈕在按照你的計(jì)劃行事。而至于按鈕調(diào)用什么方法,針對什么對象,這都不是在測試中該考慮的內(nèi)容。

UIKit 在 UIControl 里提供了非常有用的 sendActionsForControlEvents: 方法,我們可以用來模仿用戶操作。比如,用它來點(diǎn)擊按鈕:

[_button sendActionsForControlEvent: UIControlEventTouchUpInside];

類似地,調(diào)用這個(gè)函數(shù)來切換 UISegmentedControl 的選項(xiàng)卡:

segments.selectedSegmentIndex = 1;
[segments sendActionsForControlEvent: UIControlEventValueChanged];

注意在這里并不只是發(fā)送了 UIControlValueChanged 這個(gè)消息。當(dāng)一個(gè)用戶和控件交互的時(shí)候,它會先改變選中的 index 值,然后再發(fā)送 UIControlValueChanged 消息。這是一個(gè)非常好的例子,示范了如何通過代碼模擬用戶行為。

UIKit 中并不是所有的控件都有一個(gè)等價(jià)于 sendActionsForControlEvents: 的方法。但是只要有創(chuàng)造力的話,總是能找到變通的方法的。正如前面所說,最重要的是使用代碼去模擬用戶觸發(fā)了這個(gè)事件。

舉個(gè)例子,UITableView 并沒有函數(shù)用來選中單元格并且讓它去調(diào)用對應(yīng)的一系列委托方法。在示例項(xiàng)目中通過兩種方式實(shí)現(xiàn)了這個(gè)功能。

第一種方法是針對 storyboard 的:它通過手動觸發(fā)你希望的單元格來調(diào)用對應(yīng)的 segue。不幸的是,這并不能驗(yàn)證單元格都是和 segue 關(guān)聯(lián)的:

[_tableViewController performSegueWithIdentifier:@"TableViewPushSegue" sender:nil];

另一個(gè)選擇則不需要 storyboard 的參與,在測試代碼里手動調(diào)用 tableView:didSelectRowAtIndexPath: 這個(gè)委托方法。如果你使用 storyboard,你可以依舊使用segue,但是你需要從委托方法中手動調(diào)用:

[_viewController.tableView.delegate tableView:_viewController.tableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
expect(_viewController.navigationController.topViewController).to.beKindOf([PresentedViewController class]);

我更傾向于第二種選擇,它完全將測試從 ViewController 的呈現(xiàn)方式中解耦。它可以是一個(gè)自定義的 segue,或者 presentViewController:animated:completion,或者是其他的甚至 Apple 還沒發(fā)明的方式。不過,所有測試所關(guān)心的是最后的 topViewController 屬性是不是像預(yù)期的一樣。最好的選擇是讓 TableView 自己去選中一行數(shù)據(jù)并且觸發(fā)對應(yīng)的響應(yīng) action,不過現(xiàn)在這個(gè)方法行不通。

作為測試控件的最后一個(gè)示例,我想展示一下 UIBarButtonItem 的特殊情況。它們沒有 sendActionsForControlEvent: 方法,因?yàn)樗鼈儧]有繼承自 UIControl 類。讓我們看看對于這樣的情況,如何發(fā)送按鈕事件,以及,對于我們的代碼而言,如何讓它看起來像是被用戶點(diǎn)擊了。

UIBarButtonItem 并不像 UIControl,UIBarButtonItem 只擁有一個(gè) target 和一個(gè) action 與它關(guān)聯(lián)。調(diào)用這個(gè)事件很簡單:

[_viewController.barButton.target  performSelector:_viewController.barButton.action
                                         withObject:_viewController.barButton];

如果你在使用 ARC 那么編譯器會抱怨說無法從未知的 selector 中推斷出內(nèi)存管理的方式。這種狀況對我而言是不可接受的,因?yàn)樵谖已劾锞婢褪清e誤。

一個(gè)選擇是用 #pragma directive 來隱藏警告,另一個(gè)選擇就是使用直接使用runtime:

#import <objc/message.h>

objc_msgSend(_viewController.barButton.target, _viewController.barButton.action, _viewController.barButton);

我更喜歡 runtime 的方式,因?yàn)槲也幌矚g我的代碼被 pragma directives 搞得一團(tuán)糟。而且也因?yàn)樗o了我一個(gè)實(shí)際使用 runtime 的借口。

說句實(shí)話,我并不百分百的確定這些 "解決方案" 不會出問題,因?yàn)檫@并沒有解決根本的警告。測試的生命周期往往是短暫的,所以任何在測試操作中發(fā)生的內(nèi)存缺陷都不足以引起內(nèi)存問題。雖然在我使用的這段時(shí)間一直沒什么問題,但是其實(shí)我對這種情況也不是十分清楚,而且它有可能會隨機(jī)的在某個(gè)問題報(bào)出異常。如果有任何建議,歡迎在這里提出來

在文章的最后,我想再說一說 ViewController。ViewController 可能是 iOS 應(yīng)用中最重要的部分,它被抽象出來調(diào)節(jié)視圖和業(yè)務(wù)邏輯的關(guān)系。為了能更好的測試用戶行為,我們不得不呈現(xiàn) ViewController。但是,在測試用例中呈現(xiàn) ViewController 讓我很快得出結(jié)論:在構(gòu)建它們的過程中,適合測試并不在考慮之內(nèi)。

Presenting and dismissing view controllers is the best way to make sure every test has a consistent start state. Unfortunately, doing so in rapid succession—like a test runner does—will quickly result in error messages like:

顯示和隱藏 ViewController 是確保每個(gè)測試都有一個(gè)不變的初始狀態(tài)的最好方式。但是不幸的是,在連續(xù)快速的這樣做之后 -- 測試?yán)锟隙ǘ歼@么做 -- 很快就會導(dǎo)致下面的錯誤信息:

  • Warning: Attempt to dismiss from view controller <UINavigationController: 0x109518bd0> while a presentation or dismiss is in progress!
  • Warning: Attempt to present <PresentedViewController: 0x10940ba30> on <UINavigationController: 0x109518bd0> while a presentation is in progress!
  • Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0x109518bd0>
  • nested push animation can result in corrupted navigation bar

一套測試應(yīng)該盡可能的快,一直等到每一個(gè) ViewController 的展示結(jié)束是不可接受的。最終我們發(fā)現(xiàn),這些警告都是基于單窗口的。只要在獨(dú)立的窗口展示每一個(gè) ViewController,就可以給你的測試一個(gè)始終一致的開始狀態(tài),也保證它運(yùn)行起來足夠的快。通過在每個(gè)獨(dú)立的窗口展示分別,你就可以不需要等到展示或者消失過程結(jié)束了。

對于 ViewController 還有一些問題。比如,push 到導(dǎo)航控制器的操作發(fā)生在下一個(gè) run loop,而使用 modal 的方式彈出窗口卻不是這樣。如果你想嘗試一下這種測試方式,我建議你看一下我的 ViewController 測試助手,它會幫你解決這些問題。

當(dāng)測試行為的時(shí)候,你經(jīng)常需要證實(shí),在某個(gè)交互之后,一個(gè)新的 ViewController 可以正常的彈出來。換句話說,你需要證實(shí)當(dāng)前 ViewController 結(jié)構(gòu)的狀態(tài)。UIKit 在這個(gè)方面做的很好,它提供了一系列必要的方法,幫助你完成這個(gè)工作。比如下面這個(gè)例子,它可以讓你確定 ViewController 有沒有正確地以 modal 形式彈出:

expect(_viewController.presentedViewController).to.beKindOf([PresentedViewController class]);

或者以 push 進(jìn)導(dǎo)航控制器:

expect(_viewController.navigationController.topViewController).to.beKindOf([PresentedViewController class]);

UI測試其實(shí)并不難,只需要清楚你需要測試的內(nèi)容就行。你需要測試的是用戶交互,而不是應(yīng)用的外觀。通過創(chuàng)造力和不斷的堅(jiān)持,大多數(shù)框架的缺點(diǎn)都是可以通過變通的方法解決的,而不用犧牲測試套件的穩(wěn)定性和可維護(hù)性。時(shí)刻記著,讓你的測試盡可能接近用戶的真實(shí)操作。