雲端 Mac 上驗證 iOS 搜尋防抖:虛擬時間與取消邊界

雲端 Mac 上驗證 iOS 搜尋防抖:虛擬時間與取消邊界

雲端 Mac 上驗證 iOS 搜尋防抖:虛擬時間與取消邊界

搜尋框設定了 300 毫秒防抖,測試卻寫成「輸入後等待半秒,再檢查請求次數」。在雲端 Mac 的平行任務中,這種測試不僅慢,也容易受到排程延遲干擾。真正需要驗收的是:最後一次輸入後的截止時間、舊輸入被取代,以及清空後不再交付。把這些規則從計時器中抽離,測試就不必等待真實時間。

先定義搜尋的時間契約

本文採用尾緣防抖:每次非空輸入都重新計算截止時間,到期時只交付最新搜尋詞一次。先移除字串頭尾的空白;只有空白的字串與空字串都會取消待交付內容。再次輸入相同的詞也會重新計時,這是明確的產品規則,不是所有實作的預設行為。

時間以測試起點為基準,用整數毫秒表示,而不是系統日期。在正式環境中,應從單調時鐘取得經過時間,不能使用可能因校時而跳動的實際時間時鐘。300 毫秒只是範例策略,不是服務效能指標。

防抖只決定何時交付搜尋詞。是否取消已送出的網路請求、舊回應能否更新介面,是另外兩項契約,不能用「計時器已取消」取代驗收。

將計時規則寫成獨立狀態機

在雲端 Mac 的命令列建立一個小型驗證套件。目前選用的開發工具鏈必須包含 Swift;這裡不依賴模擬器或圖形介面工作階段。

mkdir SearchTiming
cd SearchTiming
swift package init --type library --name SearchTiming

將 Sources/SearchTiming/SearchTiming.swift 替換為以下內容。它不會建立執行緒或計時器;呼叫端負責依非遞減順序傳入時間,並以序列方式存取狀態。

import Foundation

struct SearchDebounce {
    let delay: Int64
    private(set) var deadline: Int64?
    private var pending: String?

    init(delay: Int64 = 300) {
        precondition(delay > 0)
        self.delay = delay
    }

    mutating func input(_ text: String, at now: Int64) {
        let query = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !query.isEmpty else {
            pending = nil
            deadline = nil
            return
        }
        pending = query
        deadline = now + delay
    }

    mutating func takeDue(at now: Int64) -> String? {
        guard let limit = deadline, now >= limit else {
            return nil
        }
        let query = pending
        pending = nil
        deadline = nil
        return query
    }
}

takeDue 會同時檢查截止時間,並取出待交付內容、將其從狀態中清除。因此,重複回呼不會重複交付,過早回呼也不會清掉內容。範例假設時間值落在正常運作範圍內;若時鐘轉接器接受外部數值,還應檢查負值、時間倒退與加法溢位。

推進虛擬時間,不呼叫 sleep

將 Tests/SearchTimingTests/SearchTimingTests.swift 替換為以下測試。每個測試案例都會建立新的狀態,避免共用時鐘與待處理輸入。

import XCTest
@testable import SearchTiming

final class SearchTimingTests: XCTestCase {
    func testDeadlineAndSingleDelivery() {
        var state = SearchDebounce()
        state.input(" swift ", at: 0)
        XCTAssertNil(state.takeDue(at: 299))
        XCTAssertEqual(state.takeDue(at: 300), "swift")
        XCTAssertNil(state.takeDue(at: 301))
    }

    func testLatestInputMovesDeadline() {
        var state = SearchDebounce()
        state.input("s", at: 0)
        state.input("sw", at: 100)
        state.input("swift", at: 220)
        XCTAssertEqual(state.deadline, 520)
        XCTAssertNil(state.takeDue(at: 300))
        XCTAssertNil(state.takeDue(at: 519))
        XCTAssertEqual(state.takeDue(at: 520), "swift")
    }

    func testBlankCancelsPendingInput() {
        var state = SearchDebounce()
        state.input("swift", at: 0)
        state.input(" \n ", at: 100)
        XCTAssertNil(state.deadline)
        XCTAssertNil(state.takeDue(at: 1000))
    }

    func testSameInputRestartsDelay() {
        var state = SearchDebounce()
        state.input("swift", at: 0)
        state.input("swift", at: 200)
        XCTAssertNil(state.takeDue(at: 300))
        XCTAssertEqual(state.takeDue(at: 500), "swift")
    }
}

執行 swift test。斷言失敗時,命令必須傳回非零結束碼,流水線不能只封存輸出就繼續執行。這些測試涵蓋截止前一刻、恰好到期,以及重複取出內容的情況;再補上一個明顯晚於截止時間的案例,即可驗證排程延遲不會造成內容永久遺失。

接上真實任務時,防範兩類取消錯誤

實際串接時,可以使用 ContinuousClock 等待截止時間。狀態機負責規則,轉接器負責喚醒。每次輸入先更新狀態,再取消舊的等待任務,並為新的截止時間建立等待任務;清空後則不再建立任務。

第一類錯誤是吞掉取消例外。若以 try? 等待後直接呼叫交付邏輯,取消路徑也會繼續執行。捕捉到取消時應直接退出,不能把取消當成「時間已到」。

第二類錯誤是舊任務遲到。每次輸入都應增加世代編號,等待任務則保存建立當下的世代;喚醒後,在同一個序列執行器上比對目前世代,只有相符時才允許取出並清除狀態中的內容。只檢查任務是否已取消並不足夠,因為取消與喚醒可能同時發生。

介面輸入、世代更新與結果交付可以統一放在主執行器上。不要在背景讀取狀態後,再非同步切回主執行器修改它,否則檢查與取出內容之間會出現空隙。虛擬測試使用毫秒只是為了方便斷言;真實等待應使用時鐘提供的期限與時長型別,避免累積換算誤差。

用少量串接測試完成驗收

純邏輯測試通過後,再檢查真實轉接器,而不是把所有案例改回長時間等待。設定足夠寬裕的測試逾時,但仍要檢查不會提前交付,且最終只交付一次;逾時只用來發現卡住的情況,不應作為精確的時間斷言。

驗收清單至少應包含:快速連續輸入只交付最後一項;清空立即撤銷待交付項目;取消例外不會進入交付路徑;舊世代的回呼會被丟棄;介面釋放後不再更新;連續執行多輪的結果一致。若業務需要對相同搜尋詞去重,應另外加入去重層,並明確定義清空後重新輸入是否允許再次搜尋。

保留提交版本、工具鏈版本與測試結束碼即可,不必記錄使用者的真實搜尋內容。最終得到的不是「等半秒大概就會過」的腳本,而是一組能指出哪項時序規則遭到破壞的斷言。

常見問題

虛擬時間測試能完全取代實際計時器測試嗎?

不能。虛擬時間驗證狀態機的截止與取消規則;仍需少量整合測試,確認實際排程器使用單調時鐘、取消正確,並在指定執行器上交付結果。

清空搜尋框後,已經啟動的請求也會自動取消嗎?

不會。本文狀態機只取消尚未交付的搜尋詞。已啟動請求需要由網路層另外取消,並以請求世代檢查阻止舊結果寫回。

為你的專案保留一台專屬 Mac

Mac mini 實體節點,非虛擬機器。可按日、週、月或季租用;先選擇用途與規格,再確認控制台即時回傳的可用狀態。

選擇機型與租期 查看完整價格