🔀UUID 的隨機特性很好,測試的困難就交給 Swift Dependencies
UUID 的隨機性讓測試變得不可預測。透過 Swift Dependencies 的 UUIDGenerator,我們可以在測試時產生遞增的 UUID,確保排序邏輯正確。還會自動識別運作環境,不影響實際 app 運作。
上一篇 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
。