iOS 應用程式將登入回呼、活動頁面、訂單詳情與通知落點交由深層連結處理後,只要一次路由重構,就可能讓連結停留在首頁,或在應用程式已開啟時重複推入同一頁面。手動點擊幾個連結只能證明當下可用,無法涵蓋冷啟動、前景狀態、編碼參數與無效輸入。更穩妥的方式,是在雲端 Mac 上固定模擬器環境,將每條深層連結的最終路由轉換成持續整合可讀取的結果。
先將連結定義成可審查的契約
不要在測試指令碼中零散地拼接 URL。應先維護一份路由清單,明確列出輸入、預期路由與應用程式狀態。
| 情境 | 輸入 | 預期結果 |
|---|---|---|
| 商品詳情 | linklab://product/42 |
product/42 |
| 搜尋參數 | linklab://search?q=swift%20ui |
search?query=swift ui |
| 缺少識別碼 | linklab://product/ |
error/invalid-product |
| 未知路徑 | linklab://unknown |
error/not-found |
清單中的預期值應採用路由器解析後的標準形式,而不是某個按鈕的標題。如此一來,介面文案變動不會造成誤報,測試也能直接指出問題出在路徑、參數還是狀態還原。
深層連結關卡驗證的是「輸入是否抵達正確的業務路由」,而不是「系統是否接受了一個 URL」。命令執行成功不代表頁面正確。
同一路由至少要準備冷啟動與前景狀態兩種案例。若涉及身分驗證,還要區分已登入與未登入狀態,但測試帳號的狀態必須由指令碼明確建立,不能沿用上一個案例。
在偵錯建置中加入可觀測探針
命令列可以開啟連結,卻無法可靠判斷應用程式最終顯示了什麼。可在 DEBUG 建置中,讓統一路由入口將標準化結果寫入快取目錄。發佈建置不應包含這段邏輯,也不要記錄權杖或完整查詢參數。
#if DEBUG
func recordResolvedRoute(_ route: String) {
let payload: NSDictionary = [
"route": route,
"recordedAt": Date().timeIntervalSince1970
]
let directory = FileManager.default.urls(
for: .cachesDirectory,
in: .userDomainMask
)[0]
let file = directory.appendingPathComponent("link-probe.plist")
payload.write(to: file, atomically: true)
}
#endif
探針應放在 URL Scheme、Universal Link 與通知跳轉都會經過的路由器之後。若分別在生命週期回呼中記錄,測試可能只能證明回呼已被觸發,卻無法證明參數經過驗證並成功導向業務頁面。
維持結果的原子性
每次執行前先刪除舊檔案,寫入時則採用原子替換。否則,應用程式崩潰或讀取時機過早,都可能讓本次測試誤讀上一次的結果。建議只記錄路由鍵、錯誤類型與時間戳記,並先將使用者輸入去識別化。
固定模擬器並執行路由矩陣
CI 不應使用含糊的 booted 作為起點。應先依裝置名稱與系統版本解析 UDID,確認只有目標模擬器啟動,再等待系統完成開機。以下假設流水線已取得 SIMULATOR_UDID。
set -euo pipefail
xcrun simctl shutdown all
xcrun simctl boot "$SIMULATOR_UDID"
xcrun simctl bootstatus "$SIMULATOR_UDID" -b
xcodebuild \
-scheme LinkLab \
-configuration Debug \
-sdk iphonesimulator \
-derivedDataPath .ci/DerivedData \
build
APP_PATH=".ci/DerivedData/Build/Products/Debug-iphonesimulator/LinkLab.app"
xcrun simctl install "$SIMULATOR_UDID" "$APP_PATH"
安裝後先取得資料容器,再逐一清除探針、開啟連結並輪詢結果。不要設定固定且冗長的 sleep;應用程式啟動速度改變時,這種做法不是浪費時間,就是依然讀不到檔案。
BUNDLE_ID="com.example.linklab"
DATA_DIR="$(xcrun simctl get_app_container \
"$SIMULATOR_UDID" "$BUNDLE_ID" data)"
PROBE="$DATA_DIR/Library/Caches/link-probe.plist"
rm -f "$PROBE"
xcrun simctl openurl "$SIMULATOR_UDID" "linklab://product/42"
for attempt in 1 2 3 4 5 6 7 8 9 10; do
test -f "$PROBE" && break
sleep 1
done
test -f "$PROBE"
ACTUAL="$(/usr/libexec/PlistBuddy -c "Print :route" "$PROBE")"
test "$ACTUAL" = "product/42"
前景案例應在第一次開啟後繼續傳送 URL;冷啟動案例則要先執行 simctl terminate。每個失敗案例都應保存輸入、預期值、實際值與測試記錄,但不要封存含有機密資訊的完整應用程式容器。
Universal Link 必須分兩層驗收
URL Scheme 適合用來穩定驗證路由器,但不能取代 Universal Link 的端對端檢查。後者還依賴關聯網域權限、HTTPS 網站檔案、內容類型、應用程式識別碼與系統快取。
第一層在每次提交時直接呼叫統一路由器,快速涵蓋路徑標準化、百分比編碼、重複參數、空值與未知路由。第二層則在受控測試網域上執行真實 HTTPS 連結,重新安裝應用程式後驗證冷啟動與前景切換。網站關聯檔案一旦變更,就必須觸發第二層,不能只執行單元測試。
常見誤區是在修改關聯檔案後立即重複點擊連結,並將舊快取結果誤認為新設定。除錯時應記錄應用程式建置編號、系統版本、安裝順序與測試網域的回應標頭;先證明系統已將連結交給應用程式,再檢查業務路由。
將失敗證據與清理動作納入關卡
可靠的關卡不只要回傳非零狀態,還必須讓維護者快速判斷故障層級。失敗摘要至少應包含案例名稱、應用程式狀態、輸入類型、預期路由、實際路由,以及探針檔案是否產生。若檔案不存在,應優先檢查安裝、生命週期入口與連結宣告;若檔案存在但路由錯誤,再檢查解析器與導覽狀態。
合併前的檢查清單可以保持簡短:
- 模擬器型號與系統版本由流水線固定;
- 冷啟動與前景狀態分別執行;
- 每個案例執行前先清理應用程式程序或探針狀態;
- 特殊字元、空參數與未知路徑都有反例;
- Universal Link 具備獨立的 HTTPS 驗收;
- 失敗產物不包含憑證、權杖與使用者資料;
- 工作結束後關閉模擬器並刪除暫存建置目錄。
最後執行 xcrun simctl shutdown "$SIMULATOR_UDID",並由工作區清理策略處理 .ci/DerivedData。只有在路由契約、可觀測探針與狀態隔離同時到位時,深層連結測試才能從「連結可以開啟」提升為能夠阻止導覽回歸的工程關卡。
常見問題
只執行 simctl openurl 就能證明 Universal Link 設定正確嗎?
不能。它主要驗證應用程式路由行為,HTTPS 網域、關聯檔案、簽署權限與系統快取仍須透過獨立環境驗收。
為什麼深層連結要分別測試冷啟動與前景狀態?
兩種狀態可能進入不同的生命週期回呼。缺少任一案例,都可能漏掉首次路由遺失或重複導航問題。
選擇獨享實體節點,滿足建置、測試與實驗需求
兩檔 M4 配置涵蓋新加坡、東京、首爾與香港節點。實際可用狀態以下單時回傳的結果為準。