付費限定

🕕時間看似單純,但很難測試,靠 Swift Dependencies 來救

更新於 發佈於 閱讀時間約 13 分鐘

Swift Dependencies 的官方文件雖然很詳細,但是我想挑戰用自己的方式去說明。就從前一篇有提到的 Date 開始說起吧!

假設我們的 app 是個筆記軟體,有一個 Note type。裡面有建立與修改筆記的時間欄位:

struct Note { // v1
let createdDate = Date()
var modifiedDate = Date()
var text: String = ""
}

這樣寫的話,兩個欄位的日期有低機率會不相同

以我在 M4 的 Mac 上反覆執行以下測試 1,000 次,有 2% 的機率會有時間差:

import Testing

@Test("Init Note and ensure modified date is the same as created date")
func initNote() {
let sut = Note()

#expect(sut.createdDate == sut.modifiedDate)
}

提示:如何在 Xcode 進行反覆測試

1. 在 Xcode 的測試上面按右鍵,選 Test Repeatedly…

在 Xcode 的測試上面按右鍵,選 Test Repeatedly…

在 Xcode 的測試上面按右鍵,選 Test Repeatedly…

2. 選 Maximum,並輸入一個較大的數字,再按 Run

選 Maximum,並輸入一個較大的數字,再按 Run

選 Maximum,並輸入一個較大的數字,再按 Run

3. 錯誤結果顯示,有 2% 的情況 createdDate 與 modifiedDate 並不相等。

錯誤結果顯示,有 2% 的情況 createdDate 與 modifiedDate 並不相等。

錯誤結果顯示,有 2% 的情況 createdDate 與 modifiedDate 並不相等。

很顯然的,Note 在 init 的時候,兩個 properties 分別呼叫了一次 Date(),而兩次得到的系統時間,可能有極細微的差異。實際情況會依照硬體的運算速度而不同。

Note() 的這種現象,我們可以稱之為 non-deterministic,也就是無法得到完全穩定的結果。

要避免這種問題,我們要修改程式碼,讓 Date() 只使用到一次。:

struct Note {
let createdDate: Date
var modifiedDate: Date

init() {
let date = Date()
self.createdDate = date
self.modifiedDate = date
}
}

再跑重複 1,000 次的測試,就完全通過了。

現在,Date() 寫在 init() 裡面,所以沒有辦法從外部修改。也許我們可以把 date 丟進 init 當參數,再設個預設值:

init(date: Date = Date()) { ... }

這樣,一切問題就解決了嗎?

修改的時候更新日期

事情沒有這麼簡單。

比如說,我想確保 Note 的文字被修改時,modifiedDate 會更新,所以我們幫 text 增加一個 didSet 來修改 modifiedDate

struct Note {
let createdDate: Date
var modifiedDate: Date
var text: String = "" {
didSet {
modifiedDate = Date()
}
}

init(date: Date = Date()) {
self.createdDate = date
self.modifiedDate = date
}
}

測試就期望 modifiedDate 要比 createdDate 更晚。

@Test("When note text updated, modifiedDate also updated")
func updateNoteText() {
var sut = Note()
sut.text = "New"

#expect(sut.modifiedDate > sut.createdDate)
}

結果,你猜怎麼樣?會有一定的錯誤率。

錯誤結果顯示,有 93% 的情況 modifiedDate 並沒有 > createdDate。

錯誤結果顯示,有 93% 的情況 modifiedDate 並沒有 > createdDate。

因為 init() 的 Date() 與 didSet 的 Date() 是分成兩次呼叫,但在測試時有可能在系統時間的最小單位還沒切換之前,就連續被呼叫,導致無法達成兩個時間之間 > 的條件。

這又是一個 non-deterministic 的情況,真頭痛。

我們先把這個問題暫時放一邊,再舉一個稍微複雜但實際的需求。

再稍微複雜一點的需求

比如說,這個筆記 app,如果遇到 Note 的最後修改時間超過一個月,在畫面上的顏色就要變淡。

這種需求屬於畫面上的邏輯,我們可以另外寫一個 computed var,放在 extension。例如:

extension Note {
var isOutdated: Bool {
// grayed out notes edited > ~one month
Date().timeIntervalSince(modifiedDate) >= 30 * 86_400
}
}

(請注意,30 * 86400 不是一個很精準的一個月的定義。比較好作法是用 Calendar 做日期的計算,不過這邊我們可以先簡化。)

如果要進行測試的話,問題就大了。我們要怎麼創造出一個修改日期超過一個月的 Note

上述的例子都還只是很簡單的 model 單元測試需求,但是遇到 Date() 這種由系統控制結果的東西,測試就變得幾乎無法撰寫,或是得不到決定性的結果。

顯然,我們不應該直接使用 Date(),而是要掌控「讀取當前系統時間」時,拿到的值。

掌控時間

透過 Swift Dependencies,我們可以把前述的 Date() 都改寫成 @Dependency(\.date.now),這是 Swift Dependencies 內建的功能。然後在測試的時候就可以覆蓋掉了。

語法如下:

以行動支持創作者!付費即可解鎖
本篇內容共 4978 字、0 則留言,僅發佈於📦 推薦 Swift Package你目前無法檢視以下內容,可能因為尚未登入,或沒有該房間的查看權限。
留言
avatar-img
留言分享你的想法!
avatar-img
13+
1.5K會員
82內容數
13 以 10+ 年 iOS 開發經驗為基礎撰寫,助你在 AI 時代成為更有自信的技術工作者。 ❤️ 支持 13 創作! 🤖 AI 工具實戰經驗與深度思考 🧠 軟體開發思維、職涯發展建議 💡 實用技巧與踩坑經驗分享 😔 開發者身心健康與職業傷害
13+的其他內容
2025/09/07
Swift Dependencies 是我最常使用的套件。它讓外部條件變成完全可控,解決了測試困難和 SwiftUI Preview 編譯緩慢的問題。使用它以後,我在區分元件職責、設計測試的功力都大幅提升。
Thumbnail
2025/09/07
Swift Dependencies 是我最常使用的套件。它讓外部條件變成完全可控,解決了測試困難和 SwiftUI Preview 編譯緩慢的問題。使用它以後,我在區分元件職責、設計測試的功力都大幅提升。
Thumbnail
2025/08/26
今年 iPlayground,我自告奮勇接下了 iOS app 的開發。開發背後的一些小故事,我覺得可以拿出來分享,就寫了這篇文章。
Thumbnail
2025/08/26
今年 iPlayground,我自告奮勇接下了 iOS app 的開發。開發背後的一些小故事,我覺得可以拿出來分享,就寫了這篇文章。
Thumbnail
2025/05/16
分享一下自己的 EDC──身為 iOS 開發者的日常兼旅行背包,都放了什麼東西。
Thumbnail
2025/05/16
分享一下自己的 EDC──身為 iOS 開發者的日常兼旅行背包,都放了什麼東西。
Thumbnail
看更多
你可能也想看
Thumbnail
透過蝦皮分潤計畫,輕鬆賺取零用金!本文分享5-6月實測心得,包含數據流程、實際收入、平臺優點及注意事項,並推薦高分潤商品,教你如何運用空閒時間創造被動收入。
Thumbnail
透過蝦皮分潤計畫,輕鬆賺取零用金!本文分享5-6月實測心得,包含數據流程、實際收入、平臺優點及注意事項,並推薦高分潤商品,教你如何運用空閒時間創造被動收入。
Thumbnail
單身的人有些會養寵物,而我養植物。畢竟寵物離世會傷心,植物沒養好再接再厲就好了~(笑)
Thumbnail
單身的人有些會養寵物,而我養植物。畢竟寵物離世會傷心,植物沒養好再接再厲就好了~(笑)
Thumbnail
不知你有沒有過這種經驗?衛生紙只剩最後一包、洗衣精倒不出來,或電池突然沒電。這次一次補貨,從電池、衛生紙到洗衣精,還順便分享使用心得。更棒的是,搭配蝦皮分潤計畫,愛用品不僅自己用得安心,分享給朋友還能賺回饋。立即使用推薦碼 X5Q344E,輕鬆上手,隨時隨地賺取分潤!
Thumbnail
不知你有沒有過這種經驗?衛生紙只剩最後一包、洗衣精倒不出來,或電池突然沒電。這次一次補貨,從電池、衛生紙到洗衣精,還順便分享使用心得。更棒的是,搭配蝦皮分潤計畫,愛用品不僅自己用得安心,分享給朋友還能賺回饋。立即使用推薦碼 X5Q344E,輕鬆上手,隨時隨地賺取分潤!
Thumbnail
身為一個典型的社畜,上班時間被會議、進度、KPI 塞得滿滿,下班後只想要找一個能夠安靜喘口氣的小角落。對我來說,畫畫就是那個屬於自己的小樹洞。無論是胡亂塗鴉,還是慢慢描繪喜歡的插畫人物,那個專注在筆觸和色彩的過程,就像在幫心靈按摩一樣,讓緊繃的神經慢慢鬆開。
Thumbnail
身為一個典型的社畜,上班時間被會議、進度、KPI 塞得滿滿,下班後只想要找一個能夠安靜喘口氣的小角落。對我來說,畫畫就是那個屬於自己的小樹洞。無論是胡亂塗鴉,還是慢慢描繪喜歡的插畫人物,那個專注在筆觸和色彩的過程,就像在幫心靈按摩一樣,讓緊繃的神經慢慢鬆開。
Thumbnail
在網路速度有限的情況下,依序記錄不斷產生的資訊,能統計使用者在頁面上操作了哪些功能。
Thumbnail
在網路速度有限的情況下,依序記錄不斷產生的資訊,能統計使用者在頁面上操作了哪些功能。
Thumbnail
本文介紹瞭如何在SwiftUI中調整元件的對齊方式,包括置中、向左/向右/向上/向下對齊的方法。透過調整HStack、VStack以及frame的maxWidth、maxHeight和alignment屬性,可以達到想要的對齊效果。
Thumbnail
本文介紹瞭如何在SwiftUI中調整元件的對齊方式,包括置中、向左/向右/向上/向下對齊的方法。透過調整HStack、VStack以及frame的maxWidth、maxHeight和alignment屬性,可以達到想要的對齊效果。
Thumbnail
本文檔介紹了在Swift中使用套件的詳細方法,包括如何引用第三方套件和自定義模組,如何創建自定義套件,以及一些常見的Swift套件。這些套件可以幫助開發者快速添加功能到項目中,提高開發效率和程式碼品質。
Thumbnail
本文檔介紹了在Swift中使用套件的詳細方法,包括如何引用第三方套件和自定義模組,如何創建自定義套件,以及一些常見的Swift套件。這些套件可以幫助開發者快速添加功能到項目中,提高開發效率和程式碼品質。
Thumbnail
本章節介紹了如何建立並設置Swift項目以及如何選擇和設置Swift代碼編輯器。這包括在Xcode和命令行中建立Swift項目,選擇Xcode、Visual Studio Code或AppCode作為編輯器,以及如何使用SPM安裝插件。
Thumbnail
本章節介紹了如何建立並設置Swift項目以及如何選擇和設置Swift代碼編輯器。這包括在Xcode和命令行中建立Swift項目,選擇Xcode、Visual Studio Code或AppCode作為編輯器,以及如何使用SPM安裝插件。
Thumbnail
這份文件的目的是介紹Swift語言,包括它的特性、應用範疇,以及誰在使用它。它也提供了一些學習Swift的資源和工具,以及一些常見的Swift庫和框架。
Thumbnail
這份文件的目的是介紹Swift語言,包括它的特性、應用範疇,以及誰在使用它。它也提供了一些學習Swift的資源和工具,以及一些常見的Swift庫和框架。
Thumbnail
需求情境: 在設計畫面時,資料來源是後台的 api,每一次畫面細節的修修改改,都會觸發 Xcode Preview 程序,導致不斷呼叫後台。此時若資料結構和大小都具有一定規模,就會導致效率低落,不斷等待,且消耗伺服器資源甚鉅。 解決方案: 將後台傳回的資料以檔案形式暫存在本地端,每次 pr
Thumbnail
需求情境: 在設計畫面時,資料來源是後台的 api,每一次畫面細節的修修改改,都會觸發 Xcode Preview 程序,導致不斷呼叫後台。此時若資料結構和大小都具有一定規模,就會導致效率低落,不斷等待,且消耗伺服器資源甚鉅。 解決方案: 將後台傳回的資料以檔案形式暫存在本地端,每次 pr
Thumbnail
Part.1 搞定基本的 UI 開始開發 iOS App。 首先準備一台 Mac,然後安裝 Xcode,新增專案,系統即刻生成基本的專案結構。coding 的起點在檔案 ContentView.swift: import SwiftUI struct ContentView: View {  
Thumbnail
Part.1 搞定基本的 UI 開始開發 iOS App。 首先準備一台 Mac,然後安裝 Xcode,新增專案,系統即刻生成基本的專案結構。coding 的起點在檔案 ContentView.swift: import SwiftUI struct ContentView: View {  
Thumbnail
只是 Swift 以 language level 支援 Optional 確實比用 API level 支援的 Java 要簡潔和更具可讀性。Swift 作為一個全新的語言,從一開始的設計就將許多好的語言特性加入,確實讓人驚豔。
Thumbnail
只是 Swift 以 language level 支援 Optional 確實比用 API level 支援的 Java 要簡潔和更具可讀性。Swift 作為一個全新的語言,從一開始的設計就將許多好的語言特性加入,確實讓人驚豔。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News