讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 9.6 測試 >

9.6 測試

測試代碼並不屬於應用目標的一部分,其目的在於確保應用運行與期望保持一致。測試可以分為如下兩類:

單元測試

單元測試會在內部執行應用目標,這是從代碼的角度來看待的。比如,單元測試可能會調用應用目標代碼的某個方法,傳給其若干參數,然後看看是否每次都能返回期望的結果,不僅僅在正常情況下,還要看不正確或極端輸入情況下是否也能如此。

界面(UI)測試

界面測試(Xcode 7新增的功能)會在外部執行應用,這是從用戶的角度來看待的。這種測試會讓應用通過一系列用例場景,用手指輕拍界面上的按鈕,觀察結果並確保界面行為與期望保持一致。

在理想情況下,測試應該伴隨著應用開發的過程來編寫和運行。在編寫實際代碼前編寫單元測試會更好一些,可作為實現算法的一種方式。在確定代碼通過了測試後,可以繼續運行這些測試來檢測在開發過程中是否引入了Bug。

測試會被打包到項目的一個單獨的目標當中(參見第6章)。借助應用模板,可以在創建項目時添加測試目標:在第2個對話框(「Choose options」,即命名項目時)中,可以勾選Include Unit Tests、Include UI Tests,或二者都勾選上。此外,可以隨時輕鬆創建新的測試目標:創建一個新的目標,並指定iOS→Test→iOS Unit TestingBundle或iOS UI Testing Bundle即可。需要顯式運行測試才行。可以在測試導航器(Command-5組合鍵)與測試類文件中輕鬆管理並運行測試。

測試類是XCTestCase(它本身又是XCTest的子類)的子類。測試方法是測試類的實例方法,它沒有返回值並且不接收參數,並且名字以test開頭。測試目標依賴於應用目標,這意味著在編譯和構建測試類之前,我們需要先編譯和構建應用目標。運行測試也會運行應用,測試目標的產物是個包,它會在應用啟動時加載到應用中。

一個測試方法需要調用一個或多個測試斷言;在Swift中,這些斷言都是全局函數,名字以XCTAssert開頭。請參閱Apple文檔Testing With Xcode的「Writing Test Classes and Methods」章節瞭解完整的函數列表,具體位於「Assertions Listed by Category」一節下。與相應的Objective-C宏不同,Swift測試斷言函數並不接收格式化字符串(NSLog所採取的方式);每個函數都接收一個簡單的消息字符串。在Swift中,標記為「針對標量」的測試斷言函數並非真的如此,因為Swift中是不存在標量的(相對於對像來說):它們會應用於使用了Equatable或Comparable的類型。

測試類還可以包含一些輔助方法,這些方法會被測試方法所調用。此外,可以重寫從XCTestCase繼承下來的4個特殊方法:

setUp類方法

只會調用一次,並且在類中所有測試方法執行之前調用。

setUp實例方法

在每個測試方法調用前調用。

tearDown實例方法

在每個測試方法調用後調用。

tearDown類方法

只會調用一次,並且在類中所有測試方法執行之後調用。

測試目標也是個目標,其產出是個包,其構建階段類似於應用目標。這意味著,如測試數據等資源可以放到包中。可以通過setUp加載這些資源;通過測試類獲得對包的引用:作為NSBundle(forClass:self.dynamicType)。

測試目標也是個模塊,就像應用目標一樣。為了能夠使用應用目標,測試目標需要將應用目標以模塊的形式導入進來。為了克服私有性限制,import語句前面應該加上@testable特性;該特性是Xcode 7中新引入的,它會將應用目標中的internal(顯式或隱式)臨時改為public。

下面編寫並運行一個單元測試方法,這是使用的是Empty Window項目。為ViewController類添加一個沒有實際意義的實例方法dogMyCats:


func dogMyCats(s:String) -> String {
    return \"\"
}
  

方法dogMyCats接收一個字符串並返回字符串\"dogs\"。但此時卻並非如此;它返回一個空字符串。這是個Bug。現在編寫一個測試方法以找出這個Bug。

在Empty Window項目中,選擇File→New→Target並指定iOS→Test→iOS Unit Testing Bundle。將產品命名為EmptyWindowTests;待測試的目標就是應用目標。單擊Finish。在項目導航器中會新建一個分組EmptyWindowTests,它包含一個測試文件EmptyWindowTests.swift。該文件中有一個測試類EmptyWindowTests,其中包含兩個測試方法的樁:testExample與testPerformanceExample;將這兩個方法註釋掉。我們打算使用一個會調用dogMyCats的測試方法將其替換,該方法會對結果作出斷言:

1.在EmptyWindowTests.swift頂部導入XCTest的地方,也需要導入應用目標:


@testable import Empty_Window

2.請在EmptyWindowTests類的聲明中添加一個實例屬性,用於存儲ViewController實例:


var viewController = ViewController
  

3.編寫測試方法。測試方法的名字要以test開頭!我們將其命名為testDogMyCats。它可以通過self.viewController訪問ViewController實例:


func testDogMyCats {
    let input = \"cats\"
    let output = \"dogs\"
    XCTAssertEqual(output,
        self.viewController.dogMyCats(input),
        \"Failed to produce (output) from (input)\")
}   

如果向老項目中添加單元測試,你可能需要做一些額外的配置。為了確保可以通過@testable特性導入應用目標,請在構建設置中找到Enable Testability並確保在調試配置中將其設為Yes。此外,編輯方案,確保構建動作在測試動作發生時只會構建測試目標。

現在可以運行測試了!有多種方式可以做到這一點。切換到測試導航器,你會看到裡面列出了測試目標、測試類與測試方法。將鼠標指針懸停在任意名字上,這時會在右側彈出一個按鈕。通過單擊恰當的按鈕,可以運行每個測試類中的所有測試、EmptyWindowTests類中的所有測試,還可以單獨運行testDogMyCats測試。不過請等一下,還有呢!回到EmptyWindowTests.swift,在類聲明與測試方法名左側的邊列中有一個菱形指示器;還可以單擊這個指示器運行測試,既可以運行這個類中的所有測試,也可以運行單個測試。要運行所有測試,還可以選擇Product→Test。

下面運行testDogMyCats。應用目標已經編譯並構建完畢;測試目標亦如此(如果其中有任意一步失敗了,測試就將無法進行,我們需要回過頭來解決編譯錯誤或構建錯誤)。應用會在模擬器中啟動,測試也將運行。

測試失敗了(我們其實知道會失敗)!錯誤說明會出現在代碼中失敗斷言的旁邊,以及問題導航器與日誌導航器中。此外,測試導航器中testDogMyCats旁邊、問題導航器、日誌導航器與EmptyWindowTests.swift類聲明旁邊與testDogMyCats的第一行還會出現紅色的X標記。

現在來修復代碼。在ViewController.swift中,將dogMyCats的返回值由空字符串修改為\"dogs\"。再次運行測試。通過!

最近運行的測試會列在報告導航器中。如果選擇了其中一個,編輯器就會顯示出兩個窗格。測試窗格會以簡單的大綱形式列出成功與失敗的測試,包括斷言失敗消息的文本。日誌窗格則會列出更為詳盡的信息;展開後會看到運行過的測試的完整控制台輸出,包括測試代碼中所輸出(print)的原始調試消息。

當測試失敗時,你可能希望在斷言失敗之處暫停。要做到這一點,請在斷點導航器中單擊底部的「+」按鈕並選擇Add Test Failure Breakpoint。這類似於異常斷點,它會在報告失敗前,在測試方法中斷言失敗那一行處暫停。接下來可以切換到被測試的方法,對其進行調試,查看其變量,從而找出失敗的原因所在。

有一個很有用的特性可以幫助你在方法與調用該方法的測試間切換:當選中方法中的某些代碼時,跳轉欄中的Related菜單就會包含進測試調用者。對於輔助窗格中的Tracking菜單來說亦如此。

在該示例中,創建了一個新的ViewController實例來初始化EmptyWindowTests的self.viewController。不過,如果測試需要引用現有的ViewController實例該怎麼辦呢?這與iOS編程中頻繁出現的實例引用是一個問題。測試代碼運行在一個包中,它會被注入運行著的應用中。這意味著它能看到應用的全局信息,如UIApplication.sharedApplication()。可以通過它得到所需的引用:


if let viewController =
    (UIApplication.sharedApplication.delegate as? AppDelegate)?
        .window?.rootViewController as? ViewController {
            // ...
}
  

將測試方法組織到測試目標(套件)與測試類中在很大程度上是為了方便;這會對測試導航器的佈局以及哪些測試會一起運行產生影響,同時每個測試類都有自己的屬性,自己的setUp方法等。要創建新的測試目標或測試類,請單擊測試導航器底部的「+」按鈕。

除了剛才介紹的簡單的單元測試類型,還有另外兩種形式的單元測試:

異步測試

可以在一個耗時操作執行完畢後回調測試方法。在測試方法中,可以通過調用expectationWithDescription來創建一個XCTestExpectation對像;然後初始化一個接收完成處理器的操作,調用waitForExpectationsWithTimeout:handler:。這樣會出現下面兩種情況之一:

操作完成

完成處理器會被調用。在完成處理器中,可以執行與操作結果相關的任何斷言,然後調用XCTestExpectation對象的fulfill。這會導致超時處理器得到調用。

操作超時

超時處理器會被調用。這樣,超時處理器都會得到調用,可以執行必要的清理工作。

性能測試

可以測試成功操作的速度。在測試方法中調用measureBlock,在塊中做一些事情(可以執行很多次,從而得到合理的時間度量樣本)。如果塊中涉及度量不想包含的創建與清理工作,那就可以調用measureMetrics:automaticallyStartMeasuring:forBlock:,然後將塊的核心包裝到startMeasuring與stopMeasuring中。

性能測試會執行塊多次,記錄每次運行時間。首次執行性能測試時會失敗,不過卻建立了基準度量。在隨後的運行中,如果標準偏差距離基準太遠,或平均時間變得過長,測試都會失敗。

現在來看看界面測試。假設Empty Window界面上依舊有一個按鈕(第7章),它有一個動作方法掛接到了ViewController方法上,會彈出一個警告框。我們會編寫一個測試,輕拍按鈕並確保會彈出警告框。向項目添加一個iOS UI Testing Bundle,命名為EmptyWindowUITests。

界面測試代碼是基於可訪問性的,該特性可以描述屏幕的界面,然後以編程的方式操縱它。它涉及3個類:XCUIElement、XCUIApplication(XCUIElement的子類)以及XCUIElementQuery。在很大程度上,你不必瞭解這些類,因為可訪問性動作是可記錄的。這意味著可以通過執行構成測試的實際動作來生成代碼。下面就來試一下:

1.在testExample樁方法中,創建一個新的空行,並將插入點置於其中。

2.選擇Editor→Start Recording UI Test(此外,還可以使用項目窗口底部調試欄上的Record按鈕)。應用會在模擬器中啟動。

3.輕拍界面上的按鈕。當警告框出現時,輕拍OK將其關閉。

4.回到Xcode,選擇Editor→Stop Recording UI Test。選擇Product→Stop會停止在模擬器中運行。

這會生成如下代碼(假設界面按鈕上的文字是「Hello」):


let app = XCUIApplication
app.buttons[\"Hello\"].tap
app.alerts[\"Howdy!\"].collectionViews.buttons[\"OK\"].tap
  

顯然,app對象是個XCUIApplication實例。buttons與alerts等屬性返回XCUIEle-mentQuery對象。對該對像進行下標計算會返回一個XCUIElement,接下來可以向其發送tap等動作方法。

現在運行測試,單擊testExample聲明邊欄上的菱形圖標。應用會在模擬器中啟動,手指會執行我們之前所執行的相同動作,輕拍界面上的第1個按鈕,當警告框出現後,輕拍OK按鈕,警告框就會消失。測試結束,應用在模擬器中停止運行。測試通過!

不過,重要的事情在於如果界面停止響應,那麼測試就不會通過。比如,在Main.storyboard中,選擇按鈕,在屬性查看器下方的Control中取消勾選Enabled。按鈕依舊在那兒,不過卻無法輕拍它;我們破壞了界面。運行測試。如期望一般,測試失敗了,報告導航器會給出原因:當進入輕拍「OK」按鈕這一步時,我們首先要進行查找「OK」按鈕的操作,嘗試兩次後失敗了,因為根本就沒有警告框。報告還會截屏,這樣我們就可以檢測到測試過程中界面的狀態了。將鼠標懸浮在「OK」按鈕上,一隻眼睛的圖標會出現。單擊它,截圖會展示出該時刻的界面,清晰地顯示出禁用的界面按鈕(沒有警告框)。

再次啟用按鈕來修復這個Bug。如果現在選擇Product→Test,那麼測試套件中的所有測試都會運行,包括單元測試與界面測試,它們都會通過。這個應用很簡單,不過卻是可用的!

如前所述,界面測試依賴於可訪問性。標準的界面對象是可訪問的,不過你所創建的其他界面可能未必如此。在nib編輯器中選擇一個界面,在身份查看器中查看其可訪問性。在模擬器中運行應用,選擇Xcode→Open Developer Tool→Accessibility Inspector來實時查看鼠標指針下方內容的可訪問性。要瞭解如何向界面對像添加有用的可訪問性特性,請參考Apple的Accessibility Programming Guide for iOS。