一個「記錄閱讀」捷徑在開發用 Mac 上找得到,換到遠端建置所用的模擬器卻沒有出現。這時先別修改業務程式碼:參數是否有效、Intent 能否執行、系統是否發現該動作,是三項不同的檢查。雲端 Mac 適合用來固定前兩項的測試環境;最後一項仍須在已安裝 App 的模擬器中驗收。
先釐清自動測試的邊界
本文以不寫入磁碟的範例動作驗證流程:輸入書籍編號、去除頭尾空白,再回傳正規化後的編號。如此便能先排除儲存權限與網路波動的影響。日後接上資料庫時,應另外測試寫入及重複執行策略,不要把「回傳成功」誤認為「資料已儲存」。
驗收分成三層:以純 Swift 規則檢查空值與不合法字元;透過 XCTest 呼叫 perform(),檢查回傳值與錯誤;最後由人工在模擬器中確認動作是否可見、參數能否填寫,以及結果能否讀取。前兩層失敗應阻止合併。第三層會受系統索引影響,適合作為版本發布前的檢查項目,而不是靠固定等待秒數編寫測試。
不要為了等捷徑出現,就在自動測試中反覆解除安裝、重新安裝 App。先證明程式碼契約正確,再排查 App 安裝、首次啟動與系統發現動作的過程。
將輸入規則抽成可直接測試的型別
以下規則接受 1–32 個 ASCII 字母、數字、底線與連字號。空白只允許出現在輸入的頭尾;其他字元會回傳明確的錯誤。範例中的 perform() 只回傳編號,不會產生持久化的副作用。
import AppIntents
import Foundation
enum ReadingIDError: LocalizedError {
case invalid
var errorDescription: String? {
"编号须为 1–32 位字母、数字、下划线或连字符"
}
}
enum ReadingID {
static func normalize(_ raw: String) throws -> String {
let value = raw.trimmingCharacters(in: .whitespacesAndNewlines)
let pattern = #"^[A-Za-z0-9_-]{1,32}$"#
guard value.range(of: pattern, options: .regularExpression) != nil else {
throw ReadingIDError.invalid
}
return value
}
}
struct CheckReadingIDIntent: AppIntent {
static var title: LocalizedStringResource = "检查书籍编号"
static var openAppWhenRun = false
@Parameter(title: "书籍编号")
var bookID: String
func perform() async throws -> some IntentResult & ReturnsValue<String> {
let normalized = try ReadingID.normalize(bookID)
return .result(value: normalized)
}
}
請將參數標題、動作標題及錯誤提示納入專案的本地化資源。程式碼中的簡體中文只是範例文案,不應直接當成多語言 App 所有語言的交付文字。規則本身只接受 ASCII 編號;若業務允許漢字或其他文字,應先定義允許的字元集,再修改運算式與測試案例。
覆蓋邊界條件,而不只測正常輸入
至少要檢查 " book_7 " 會回傳 "book_7",而空字串、含有中間空白的字串,以及超過 32 個字元的編號都會被拒絕。錯誤分支應斷言錯誤型別,不要只斷言「有某種錯誤被拋出」;否則底層相依項意外拋錯,也可能讓測試通過。測試 perform() 時,還要確認回傳值符合正規化規則。將純規則測試與 Intent 呼叫測試分開,失敗時才能判斷是輸入規則改變,還是系統介面的封裝層改變。
註冊入口並在模擬器中檢查
若要讓系統顯示建議動作,可以在 App target 中加入 AppShortcutsProvider。短語中保留 App 名稱的佔位符,讓系統將動作關聯到目前的 App;這不是 shell 字串。
import AppIntents
struct ReadingShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: CheckReadingIDIntent(),
phrases: ["在 \(.applicationName) 检查书籍编号"],
shortTitle: "检查书籍编号",
systemImageName: "book"
)
}
}
先確認這份檔案屬於 App target,而非只屬於測試 target。安裝建置完成的 App 並啟動一次,再開啟模擬器中的「捷徑」,搜尋動作標題,填入有效編號並執行。接著提交無效編號,檢查錯誤訊息是否易懂。若動作未出現,記錄系統版本、App 建置版本,以及安裝後的操作順序;不要把「重新開啟頁面後碰巧出現」寫成確定有效的修復方法。
固定測試裝置與結果目錄
在 ZSHMac 的雲端 Mac 上,先以 xcrun simctl list devices available 查看已安裝且可用的模擬器,選定裝置 UDID,並設定 SIM_UDID。以下命令假設專案中有名為 Reader 的共用 scheme;實際執行時,請改用自己專案的 scheme。每次執行都為結果套件指定新路徑,避免既有檔案導致測試命令直接失敗。
mkdir -p artifacts
test -n "${SIM_UDID:-}" || { printf '%s\n' 'SIM_UDID is required' >&2; exit 1; }
result="$PWD/artifacts/app-intents-$(date +%Y%m%d-%H%M%S)-$$.xcresult"
xcodebuild test \
-scheme Reader \
-destination "platform=iOS Simulator,id=$SIM_UDID" \
-resultBundlePath "$result"
在 CI 中保留命令的結束代碼與結果套件:結束代碼決定作業是否通過,結果套件則供失敗後定位測試案例。若 scheme 未共用,或目標模擬器不可用,應先修正執行入口,不要將其歸類為 Intent 邏輯失敗。手動驗收時,則記錄動作是否出現、輸入提示是否正確,以及有效與無效參數的實際表現。
失敗時依層次排查
| 現象 | 優先檢查 | 不要先做 |
|---|---|---|
| 純規則測試失敗 | 字元範圍、長度與頭尾空白處理 | 重新安裝模擬器 |
perform() 結果不符 |
傳入的參數與回傳值型別 | 等待系統索引 |
| 測試命令找不到裝置 | 可用裝置清單與 UDID | 修改 Swift 規則 |
| 捷徑中沒有動作 | App target 成員關係、安裝與首次啟動 | 在測試中加入固定延遲 |
| 動作存在但執行時報錯 | 參數提示、錯誤型別與目標系統版本 | 吞掉所有錯誤 |
分層檢查的價值在於縮小故障範圍,而不是保證系統入口在安裝後立即可見。程式碼測試通過,且模擬器中的動作也完成有效與無效輸入檢查,這次驗收才算完整。接入真正的寫入功能後,還須另外測試重複執行與資料寫入磁碟,不能沿用本例「只回傳編號」的通過標準。
常見問題
XCTest 通過後,為何捷徑仍找不到動作?
先確認包含 AppShortcutsProvider 的 App 已安裝並啟動,再檢查系統版本與目標設定。系統索引可能延遲,不能將立即顯示當成單元測試斷言。
參數驗證應放在哪一層?
把可重用的格式規則放進獨立 Swift 型別,並在 perform() 呼叫它;測試便不必依賴系統索引。
無介面的 CI 能取代捷徑驗收嗎?
不能。CI 可檢查輸入與回傳契約;動作是否顯示、參數提示是否正確,仍須在目標系統的模擬器或裝置上驗證。
需要一台獨享雲端 Mac 嗎?
選擇機型、節點和計費週期。交付的是獨享實體機,實際可用狀態以控制台即時回報為準。