- 原文地址:Avoiding force unwrapping in Swift unit tests
- 原文做者:John
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:RickeyBoy
- 校對者:YinTokey
強制解析(使用 !
)是 Swift 語言中不可或缺的一個重要特色(特別是和 Objective-C 的接口混合使用時)。它迴避了一些其餘問題,使得 Swift 語言變得更加優秀。好比 處理 Swift 中非可選的可選值類型 這篇文章中,在項目邏輯須要時使用強制解析去處理可選類型,將致使一些離奇的狀況和崩潰。前端
因此儘量地避免使用強制解析,將有助於搭建更加穩定的應用,而且在發生錯誤時提供更好的報錯信息。那麼若是是編寫測試時,狀況會怎麼樣呢?安全地處理可選類型和未知類型須要大量的代碼,那麼問題就在於咱們是否願意爲編寫測試作全部的額外工做。這就是咱們這周將要探討的問題,讓咱們開始深刻研究吧!android
當編寫測試代碼時,咱們常常明確區分測試代碼和產品代碼。儘管保持這兩部分代碼的分離十分重要(咱們不但願意外地讓咱們的模擬測試對象成爲 App Store 上架的部分😅),但就代碼質量來講,沒有必要進行明顯區分。ios
若是你思考一下的話,咱們想要對移交給使用者的代碼進行高標準的要求,緣由是什麼呢?git
如今若是反過來考慮咱們的測試,咱們想要避免哪些事情呢?github
你可能已經理解我所講的內容了 😉。express
以前很長的時間,我曾認爲測試代碼只是一些我快速堆砌的代碼,由於有人告訴我必需要編寫測試。我不那麼在意它們的質量,由於我將它視爲一件雜事,並不將它放在首位。然而,一旦我由於編寫測試而發現驗證本身的代碼有多麼快,以及對本身有多麼自信 —— 我對測試的態度就開始了轉變。swift
所如今我相信對於測試代碼,和將要移交的產品代碼進行同等的高標準要求是很是重要的。由於咱們配套的測試是須要咱們長期使用、拓展和掌握的,咱們理應讓這些工做更容易完成。後端
那麼這一切與 Swift 中的強制解析有什麼關係呢?🤔安全
有時必需要強制解析,很容易編寫一個 「go-to solution」 的測試。讓咱們來看一個例子,測試 UserService
實現的登錄機制是否正常工做:bash
class UserServiceTests: XCTestCase {
func testLoggingIn() {
// 爲了登錄終端
// 構建一個永遠返回成功的模擬對象
let networkManager = NetworkManagerMock()
networkManager.mockResponse(forEndpoint: .login, with: [
"name": "John",
"age": 30
])
// 構建 service 對象以及登陸
let service = UserService(networkManager: networkManager)
service.login(withUsername: "john", password: "password")
// 如今咱們想要基於已登錄的用戶進行斷言,
// 這是可選類型,因此咱們對它進行強制解析
let user = service.loggedInUser!
XCTAssertEqual(user.name, "John")
XCTAssertEqual(user.age, 30)
}
}
複製代碼
如你所見,在進行斷言以前,咱們強制解析了 service 對象的 loggedInUser
屬性。像上面這樣的作法並非絕對意義上的錯,可是若是這個測試由於一些緣由開始失敗,就可能會致使一些問題。
假設某人(記住,「某人」可能就是「將來的你本身」😉)改變了網絡部分的代碼,致使上述測試開始崩潰。若是這樣的事情發生了,錯誤信息可能只會像下面這樣:
Fatal error: Unexpectedly found nil while unwrapping an Optional value
複製代碼
儘管用 Xcode 本地運行時這不是個大問題(由於錯誤會被關聯地顯示 —— 至少在大多數時候 🙃),但當連續地總體運行整個項目時,它可能問題重重。上述的錯誤信息可能出如今巨大的「文字牆」中,致使難以看出錯誤的來源。更嚴重的是,它會阻止後續的測試被執行(由於測試進程會崩潰),這將致使修復工做進展緩慢而且使人煩躁。
一個潛在的解決上述問題的方式是簡單地使用 guard
聲明,優雅地解析問題中的可選類型,若是解析失敗再調用 XCTFail
便可,就像下面這樣:
guard let user = service.loggedInUser else {
XCTFail("Expected a user to be logged in at this point")
return
}
複製代碼
儘管上述作法在某些狀況下是正確的作法,但事實上我推薦避免使用它 —— 由於它向你的測試中增長了控制流。爲了穩定性和可預測性,你一般但願測試只是簡單的遵循 given,when,then 結構,而且增長控制流會使得測試代碼難於理解。若是你真的很是倒黴,控制流可能成爲誤報的起源(對此以後的文章會有更多的相關內容)。
另外一個方法是讓可選類型一直保持可選。這在某些使用狀況下徹底可用,包括咱們 UserManager
的例子。由於咱們對已經登陸的 user 的 name
和 age
屬性使用了斷言,若是任意一個屬性爲 nil
,咱們會自動獲得錯誤提示。同時若是咱們對 user 使用額外的 XCTAssertNotNil
檢查,咱們就能獲得一個很是完整的診斷信息。
let user = service.loggedInUser
XCTAssertNotNil(user, "Expected a user to be logged in at this point")
XCTAssertEqual(user?.name, "John")
XCTAssertEqual(user?.age, 30)
複製代碼
如今若是咱們的測試開始出錯了,咱們就能獲得以下信息:
XCTAssertNotNil failed - Expected a user to be logged in at this point
XCTAssertEqual failed: ("nil") is not equal to ("Optional("John")")
XCTAssertEqual failed: ("nil") is not equal to ("Optional(30)")
複製代碼
這讓咱們可以更加容易地知道發生錯誤的地方,以及該從哪裏入手去調試、解決這個錯誤 🎉。
第三個選擇在某些狀況下是很是有用的,就是將返回可選類型的 API 替換爲 throwing API。Swift 中的 throwing API 的優雅之處在於,須要時它可以很是容易地被當成可選類型使用。因此不少時候選擇採用 throwing 方法,不須要犧牲任何的可用性。好比說,假設咱們有一個 EndpointURLFactory
類,被用來在咱們的 app 中生成特定終端的 URL,這顯然會返回可選類型:
class EndpointURLFactory {
func makeURL(for endpoint: Endpoint) -> URL? {
...
}
}
複製代碼
如今咱們將其轉換爲採用 throwing API,像這樣:
class EndpointURLFactory {
func makeURL(for endpoint: Endpoint) throws -> URL {
...
}
}
複製代碼
當咱們仍然想獲得一個可選類型的 URL 時,咱們只須要使用 try?
命令去調用它:
let loginEndpoint = try? urlFactory.makeURL(for: .login)
複製代碼
就測試而言,上述這種作法的最大好處在於能夠在測試中輕鬆地使用 try
,而且使用 XCTest runner 徹底能夠毫無代價地處理無效值。這是不爲人知的,但事實上 Swift 測試能夠是 throwing 函數,看看這個:
class EndpointURLFactoryTests: XCTestCase {
func testSearchURLContainsQuery() throws {
let factory = EndpointURLFactory()
let query = "Swift"
// 由於咱們的測試函數是 throwing,這裏咱們能夠簡單地採用 'try'
let url = try factory.makeURL(for: .search(query))
XCTAssertTrue(url.absoluteString.contains(query))
}
}
複製代碼
沒有可選類型,沒有強制解析,某些發生錯誤的時候也能完美地作出診斷 👍。
然而,並非全部返回可選類型的 API 均可以被替換爲 throwing。不過在寫包含可選類型的測試時,有一個和 throwing API 一樣好的方法。
讓咱們回到最開始 UserManager
的例子。若是既不對 loggedInUser
進行強制解析,又不把它看做可選類型,那麼咱們能夠簡單地這樣作:
let user = try require(service.loggedInUser)
XCTAssertEqual(user.name, "John")
XCTAssertEqual(user.age, 30)
複製代碼
這實在是太酷了!😎這樣咱們能夠擺脫大量的強制解析,同時避免讓咱們的測試代碼難於編寫、難於上手。那麼爲了達到上述效果咱們應該怎麼作呢?這很簡單,咱們只須要對 XCTestCase
增長一個拓展,讓咱們分析任何可選類型表達式,而且返回非可選的值或者拋出一個錯誤,像這樣:
extension XCTestCase {
// 爲了可以輸出優雅的錯誤信息
// 咱們遵循 LocallizedErrow
private struct RequireError<T>: LocalizedError {
let file: StaticString
let line: UInt
// 實現這個屬性很是重要
// 不然測試失敗時咱們沒法在記錄中優雅地輸出錯誤信息
var errorDescription: String? {
return "😱 Required value of type \(T.self) was nil at line \(line) in file \(file)."
}
}
// 使用 file 和 line 使得咱們可以自動捕獲
// 源代碼中出現的相對應的表達式
func require<T>(_ expression: @autoclosure () -> T?,
file: StaticString = #file,
line: UInt = #line) throws -> T {
guard let value = expression() else {
throw RequireError<T>(file: file, line: line)
}
return value
}
}
複製代碼
如今有了上述內容,若是咱們 UserManager
登陸測試發生失敗,咱們也能獲得一個很是優雅的錯誤信息,告訴咱們錯誤發生的準確位置。
[UserServiceTests testLoggingIn] : failed: caught error: 😱 Required value of type User was nil at line 97 in file UserServiceTests.swift.
複製代碼
你可能意識到這個技巧來源於個人迷你框架 Require, 它對全部可選類型增長了一個 require() 方法,以提升對沒法避免的強制解析的診斷效果。
以一樣謹慎的態度對待你的應用代碼和測試代碼,在最開始可能有些不適應,但可讓長期維護測試變的更加簡單 —— 不管是獨立開發仍是團隊開發。良好的錯誤診斷和錯誤信息是其中特別重要的一部分,使用本文中的一些技巧或許可以讓你在將來避免不少奇怪的問題。
我在測試代碼中惟一使用強制解析的時候,就是在構建測試案例的屬性時。由於這些老是在 setUp
中被建立、tearDown
中被銷燬,我並不把他們看成真正的可選類型。正如以往,你一樣須要查看你本身的代碼,根據你本身的喜愛,來權衡決定。
因此你以爲呢?你會採用一些本文中的技巧,仍是你已經用了一些相關的方式?請讓我知道,包括你可能有的任何的問題、評價和反饋 —— 能夠在下面回覆欄直接回復或者在 Twitter @johnsundell 上回復我。
感謝閱讀!🚀
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。