在談 Unit Test 之前,先談談 SOLID 原則
• 你如果寫了一個 function (method),讓他在裡面處理了 http request,再處理了 parse data,再處理了 DB 寫入。會怎樣呢?
func downloadAndParseAndWriteInDB() {
/// 從這個 url 拿到 user 的資訊
guard let url = URL(string: "https://mydomain.com/getUserInfo") else {
print("invalid url")
/// handle error here...
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: url) { data, response, error in
/// handle error....
/// 如果 response 有 data, 後端的 spec 為 [String: Any] 的 json,但通常我們會建立好物件
/// 使用物件在 app 內部進行傳接值,而不是 dict 傳接值
if let data {
do {
let userInfo = try JSONDecoder().decode(UserInfo.self, from: data)
/// 這個 userInfo 物件有三個值: userID, firstName, lastName
print("user id: \(userInfo.userID)")
print("user firstName: \(userInfo.firstName)")
print("user lastName: \(userInfo.lastName)")
/// 寫入 Database
LocalStorageAdapter.write(userInfo: userInfo)
} catch {
print("json decode error: \(error)")
}
}
}.resume()
}
1.這段程式碼耦合了 request, parse, I/O
2.只要需求變更,你至少就要檢查 12 行 到 32 行
3.如果你對這個 func 寫了 test,當他 fail 的時候,你需要判斷這三十行左右的程式碼中,錯誤是發生在 URLRequest 階段,還是解析 JSON,還是在寫 Database 的時候產生的。
可以被測試的程式碼
讓 Data Requester 進行 request, 讓 Data Parser 處理 Parser, 讓 DB writer 做讀寫。這三個 function 才能被測試,當你的子 function 都能被測試的時候,會有兩大優勢。
func testableDownloadAndParseAndWriteInDB() async {
/// 從這個 url 拿到 user 的資訊
guard let url = URL(string: "https://mydomain.com/getUserInfo") else {
print("invalid url")
/// handle error here...
return
}
do {
let data = try await NetworkManager.shared.getUserInfo().get()
let userInfo = DataParser().parseToUserInfo(from: data)
LocalStorageAdapter.write(userInfo: userInfo)
} catch {
/// handle error here
print("Got error: \(error)")
}
}
1.只要這段程式碼出問題,你可以知道問題發生在 NetworkManager ? Or parser ? Or LocalStorageAdapter?
2. 進行需求變更的時候,你只要針對變更部分進行程式碼變更就好,你不用掃過三個子 func
舉例來說,當需求變更時,後端增加 user model key 值的時候, 你覺得 NetworkManager 和 LocalStorageAdapter 要變更嗎?