日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Swift之深入解析如何避免单元测试中的强制解析

發布時間:2024/5/21 编程问答 52 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Swift之深入解析如何避免单元测试中的强制解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、前言

  • 強制解析(使用 !)是 Swift 語言中不可或缺的一個重要特點(特別是和 Objective-C 的接口混合使用時),它回避了一些其他問題,使得 Swift 語言變得更加優秀。
  • 比如在我的博客 Swift之深入解析如何處理非可選的可選項類型 中,在項目邏輯需要時使用強制解析去處理可選類型,將導致一些離奇的情況和崩潰。
  • 因此,盡可能地避免使用強制解析,將有助于搭建更加穩定的應用,并且在發生錯誤時提供更好的報錯信息。那么如果是編寫測試時,情況會怎么樣呢?安全地處理可選類型和未知類型需要大量的代碼,問題就在于我們是否愿意為編寫測試做所有的額外工作,這就是我們需要探討的問題。

二、測試代碼 vs 產品代碼

  • 當編寫測試代碼時,我們經常明確區分測試代碼和產品代碼,盡管保持這兩部分代碼的分離十分重要(我們不希望意外地讓模擬測試對象成為 App Store 上架的部分),但就代碼質量來說,沒有必要進行明顯區分。
  • 如果思考一下的話,想要對移交給使用者的代碼進行高標準的要求,原因是什么呢?
    • 想要 App 為使用者穩定、流暢地運行;
    • 想要 App 在未來易于維護和修改;
    • 想要更容易讓新人融入我們的團隊。
  • 現在如果反過來考慮我們的測試,想要避免哪些事情呢?
    • 測試不穩定、脆弱、難于調試;
    • 當我們的 App 增加了新功能時,測試代碼需要花費大量時間來維護和升級;
    • 測試代碼對于加入團隊的新人來說難于理解。
  • 之前很長的時間,我曾認為測試代碼只是一些我快速堆砌的代碼,因為有人告訴我必須要編寫測試。我不那么在乎它們的質量,因為我將它視為一件瑣事,并不將它放在首位。然而,一旦我因為編寫測試而發現驗證自己的代碼有多么快,以及對自己有多么自信,我對測試的態度就開始了轉變。
  • 所現在我相信對于測試代碼,和將要移交的產品代碼進行同等的高標準要求是非常重要的,因為我們配套的測試是需要長期使用、拓展和掌握的,理應讓這些工作更容易完成。

三、強制解析的問題

  • 那么這一切與 Swift 中的強制解析有什么關系呢?有時必須要強制解析,很容易編寫一個 “go-to solution” 的測試,來看一個例子,測試 UserService 實現的登陸機制是否正常工作:
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 聲明,優雅地解析問題中的可選類型,如果解析失敗再調用 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)")
  • 這讓我們能夠更加容易地知道發生錯誤的地方,以及該從哪里入手去調試、解決這個錯誤。

六、使用 throw 的測試

  • 第三個選擇在某些情況下是非常有用的,就是將返回可選類型的 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))} }
  • 沒有可選類型,沒有強制解析,某些發生錯誤的時候也能完美地做出診斷。

七、使用 require 的可選類型

  • 然而,并不是所有返回可選類型的 API 都可以被替換為 throwing,不過在寫包含可選類型的測試時,有一個和 throwing API 同樣好的方法。
  • 回到最開始 UserManager 的例子,如果既不對 loggedInUser 進行強制解析,又不把它看作可選類型,那么可以簡單地這樣做:
let user = try require(service.loggedInUser) XCTAssertEqual(user.name, "John") XCTAssertEqual(user.age, 30)
  • 這實在是太酷了,這樣就可以擺脫大量的強制解析,同時避免讓測試代碼難于編寫、難于上手。那么為了達到上述效果應該怎么做呢?這很簡單,只需要對 XCTestCase 增加一個拓展,讓我們分析任何可選類型表達式,并且返回非可選的值或者拋出一個錯誤,像這樣:
extension XCTestCase {// 為了能夠輸出優雅的錯誤信息// 遵循 LocallizedErrowprivate struct RequireError<T>: LocalizedError {let file: StaticStringlet 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 中被銷毀,我并不把它們當作真正的可選類型。正如以往,你同樣需要查看你自己的代碼,根據你自己的喜好,來權衡決定。

九、參考資料

  • Handling non-optional optionals in Swift。

總結

以上是生活随笔為你收集整理的Swift之深入解析如何避免单元测试中的强制解析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。