上一篇 Swift Dependencies 文章以 Date
為範例,相信讀者已經體會到,即使是時間這麼基本的型別,也可以造成測試上的困難。而 Swift Dependencies 可以在測試環境下,替換掉當下的系統時間,使得測試時的行為能夠符合預期。
在這篇文章中,我們繼續延伸昨天的例子,加深印象的同時多介紹一點概念。
在一個筆記 app 中,指定 id
來存取特定一篇筆記,是很常見的需求。所以,我們打算把 Note
加上 id
欄位,並且使用 UUID
型別,以確保唯一性。
UUID 唯一性的用途
UUID
的特性就是在產生時會有唯一性、不會重複。這對於確保資料之間不會衝突,是非常好的特性。
但是,如果我們直接寫 let id = UUID()
,就會跟上一篇的 Date()
發生一樣的問題──無法控制產生的結果。
struct Note { // v5
let id = UUID() // 新增 id 欄位,隨機產生
let createdDate: Date
var modifiedDate: Date
var text: String = "" {
didSet {
@Dependency(\.date.now) var date
modifiedDate = date
}
}
init() {
@Dependency(\.date.now) var date
self.createdDate = date
self.modifiedDate = date
}
}
不控制 UUID 產生的結果,就不好測試
大部分情況,其實我們並不在意 UUID
的值,因為相信系統會做到產生唯一、不衝突的值,就夠了。
但假如我們把它當成某個關鍵邏輯的參考依據,那麼這種隨機特性,就會讓測試失敗。
舉例來說,我們會想要筆記有個穩定的排序規則,依照 modifiedDate
、text
、id
這三個條件。理論上時間與內文是有可能重複的(比如我們做了複製同一則筆記的功能),所以最後就以 id
為依據。
我們可以幫 Note
簡單地實作 Comparable
protocol。如果你不熟悉這個比較 tuple 的語法,簡單來說它會由左到右依序比對 modifiedDate
、text
、最後才是 id
。
extension Note: Comparable {
static func < (lhs: Self, rhs: Self) -> Bool {
(lhs.modifiedDate, lhs.text, lhs.id) < (rhs.modifiedDate, rhs.text, rhs.id)
}
}
但是在測試時,無法得到穩定的結果。
@Test(
"[non-deterministic] Ensure note default sorting order - will fail randomly"
)
func sortNotesWillFail() {
let notes = withDependencies {
$0.date.now = .init(timeIntervalSinceReferenceDate: 0)
} operation: {
[Note(), Note(), Note()]
}
let sorted = notes.sorted(by: <)
// UUID() 是隨機的,即使 `modifiedDate`、`text` 相同,也無法預測排序後的順序
#expect(notes == sorted)
#expect(notes[0] == sorted[0])
}
💡技術細節:
各種程式語言的 UUID 主要是參照 RFC 4122,並且有多種版本。
有幾個版本的 UUID,具備 time-ordered 的特性(例如 UUIDv7),產出的 id 直接當字串來排序,就具備時間遞增的效果。這類 UUID 能兼顧唯一性與遞增排序,特別適用於一些資料庫的應用。
而 Swift Foundation 的UUID
是依照 RFC 4122 version 4 的規格產生的。它會參考當下時間產生,但是產出的隨機字串並沒有遞增特性。所以連續呼叫的UUID()
的結果,不會是穩定的順序。
透過 Swift Dependencies 的 UUIDGenerator 來控制產生結果
就像 Date
,Swift Dependencies 也內建 UUID
的注入方式。方法是:@Dependency(\.uuid) var uuid
。