You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift:如何测试嵌套在定时器中的异步函数?

嘿,我懂你这种头疼的感觉——定时器里套异步任务,测试的时候总是赶在异步操作完成前就跑完断言,结果每次都失败对吧?别担心,Swift单元测试里有几种标准方法能解决这个问题,我结合你的代码给你拆解下:

方案1:给Mock API调度器加异步等待机制

你的核心问题是测试代码不知道异步上传什么时候完成,所以最直接的办法是给你的MockApiDispatcher加一个能让测试等待的方法,等上传操作完成后再执行断言。

先修改你的MockApiDispatcher:

class MockApiDispatcher {
    var sentToAPICount = 0
    private let completionSemaphore = DispatchSemaphore(value: 0)
    
    // 这个方法在上传完成时调用(业务代码里要触发它)
    func markUploadCompleted() {
        sentToAPICount += 1
        completionSemaphore.signal()
    }
    
    // 测试用的异步等待方法,超时时间可以按需调整
    func waitForUploadCompletion(timeout: TimeInterval = 2) async throws {
        try await withCheckedThrowingContinuation { continuation in
            DispatchQueue.global().async {
                let result = self.completionSemaphore.wait(timeout: .now() + timeout)
                if result == .success {
                    continuation.resume()
                } else {
                    continuation.resume(throwing: NSError(domain: "TestError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Upload timed out"]))
                }
            }
        }
    }
}

然后调整你的业务代码,在上传完成后调用这个标记方法:

self.timerProvider.scheduledTimer(withTimeInterval: timeBetweenAPISendsInSeconds, repeats: true) { _ in
    Task {
        do {
            _ = try await URLSession.shared.upload(for: request, from: encodedSessionBatch)
            self.apiDispatcher.markUploadCompleted() // 这里触发完成标记
        } catch {
            // 可选:处理错误,也可以加错误标记方法
        }
    }
}

最后修改测试代码,先等待上传完成再断言:

func test_scheduledTimer_sendsSessionBatchToAPI() async throws {
    MockTimer.currentTimer.fire()
    try await mockApiDispatcher.waitForUploadCompletion()
    XCTAssertEqual(mockApiDispatcher.sentToAPICount, 1)
}

方案2:用XCTestExpectation配合回调

如果不想用信号量,也可以用XCTest原生的XCTestExpectation,只需要给你的API调度器加一个可选的完成回调。

先给MockApiDispatcher加回调属性:

class MockApiDispatcher {
    var sentToAPICount = 0
    var onUploadComplete: (() -> Void)?
    
    func markUploadCompleted() {
        sentToAPICount += 1
        onUploadComplete?()
    }
}

然后测试代码里创建预期,等待回调触发:

func test_scheduledTimer_sendsSessionBatchToAPI() {
    let uploadExpectation = XCTestExpectation(description: "API upload finishes")
    
    mockApiDispatcher.onUploadComplete = {
        uploadExpectation.fulfill()
    }
    
    MockTimer.currentTimer.fire()
    
    // 等待预期完成,超时时间设合理值
    wait(for: [uploadExpectation], timeout: 5)
    
    XCTAssertEqual(mockApiDispatcher.sentToAPICount, 1)
}

方案3:跟踪定时器里的Task并等待它完成

如果你的业务代码允许,可以让MockTimer捕获定时器触发时创建的Task,然后测试代码直接await这个Task的完成。

先修改MockTimer来跟踪Task:

class MockTimer {
    static let currentTimer = MockTimer()
    private(set) var lastTriggeredTask: Task<Void, Error>?
    private var storedBlock: ((Timer) -> Void)?
    
    func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer {
        storedBlock = block
        return Timer() // 返回空Timer就行,测试用不到实际定时器逻辑
    }
    
    func fire() {
        storedBlock?(Timer())
    }
    
    // 供业务代码调用,保存创建的Task
    func trackTask(_ task: Task<Void, Error>) {
        lastTriggeredTask = task
    }
}

然后调整业务代码,创建Task后告诉MockTimer:

self.timerProvider.scheduledTimer(withTimeInterval: timeBetweenAPISendsInSeconds, repeats: true) { _ in
    let uploadTask = Task {
        _ = try await URLSession.shared.upload(for: request, from: encodedSessionBatch)
        mockApiDispatcher.sentToAPICount += 1
    }
    // 只在测试环境下跟踪Task,避免污染生产代码
    #if DEBUG
    (self.timerProvider as? MockTimer)?.trackTask(uploadTask)
    #endif
}

最后测试代码里await这个Task:

func test_scheduledTimer_sendsSessionBatchToAPI() async throws {
    MockTimer.currentTimer.fire()
    if let task = MockTimer.currentTimer.lastTriggeredTask {
        try await task.value
    }
    XCTAssertEqual(mockApiDispatcher.sentToAPICount, 1)
}

这三个方案里,方案1和2是比较通用的,不需要太多修改业务逻辑;方案3更适合需要精确控制Task生命周期的场景。你可以根据自己的代码结构选最顺手的那个。

内容的提问来源于stack exchange,提问作者PlankTon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 19:00:53