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

Swift中Mock TimerPublisher遇autoconnect调用失败问题及测试咨询

问题背景

我正在测试使用Apple的Timer.publish(every:on:in:)的客户端代码,希望在单元测试中控制时间,避免使用wait(for:timeout:)实现同步测试。查阅文档得知TimerPublisher符合ConnectablePublisher协议,因此编写了Fake定时器及工厂的样板代码:

import Foundation
import Combine

protocol TimerFactory {
    static func makeTimer() -> any ConnectablePublisher<Date, Never>
}

enum FakeTimerFactory: TimerFactory {
    static func makeTimer() -> any ConnectablePublisher<Date, Never> {
        return FakeTimer()
    }
}

struct FakeTimer: ConnectablePublisher {
    func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Date == S.Input {
        
    }
    
    typealias Output = Date
    typealias Failure = Never
    
    func connect() -> Cancellable {
        return FakeCancellable()
    }
    
    struct FakeCancellable: Cancellable {
        func cancel() {
            
        }
    }
}

但调用autoconnect()时触发报错:

FakeTimerFactory.makeTimer()
    .autoconnect()
    .sink { _ in
        print("Tick!")
    }

报错信息:

Member 'autoconnect' cannot be used on value of type 'any ConnectablePublisher<Date, Never>'; consider using a generic constraint instead

我希望这段代码能直接替代Timer.publish(every:on:in:)的使用,同时兼容RealTimerFactory以便在测试和生产代码间切换:

enum RealTimerFactory: TimerFactory {
    static func makeTimer() -> any ConnectablePublisher<Date, Never> {
        return Timer.publish(every: 1.0, on: .main, in: .default)
    }
}

final class UsesTimers {
    var timerFactory: TimerFactory.Type
    
    init(timerFactory: TimerFactory.Type = RealTimerFactory.self) {
        self.timerFactory = timerFactory
    }
    
    func doSomethingWithTimers() {
        timerFactory.makeTimer()
            .autoconnect() // 错误发生在这里
            .sink { _ in
                print("Tick")
            }
    }
}

我的问题:

  1. 为何autoconnect()无法使用?
  2. 有没有更好的测试TimerPublisher的方法?

问题1:为何autoconnect()无法使用?

autoconnect()是ConnectablePublisher协议的泛型扩展方法,而你返回的any ConnectablePublisher<Date, Never>是类型擦除后的存在类型(existential type)。Swift的泛型扩展无法直接作用于存在类型——编译器无法确定具体的实现类型,也就无法解析泛型约束。

解决方法

方案1:给工厂协议添加泛型约束

修改TimerFactory协议,让makeTimer()返回具体的关联类型,避免类型擦除:

import Foundation
import Combine

protocol TimerFactory {
    associatedtype TimerPublisher: ConnectablePublisher where TimerPublisher.Output == Date, TimerPublisher.Failure == Never
    static func makeTimer() -> TimerPublisher
}

enum FakeTimerFactory: TimerFactory {
    static func makeTimer() -> FakeTimer {
        return FakeTimer()
    }
}

enum RealTimerFactory: TimerFactory {
    static func makeTimer() -> Timer.TimerPublisher {
        return Timer.publish(every: 1.0, on: .main, in: .default)
    }
}

同时调整UsesTimers为泛型类:

final class UsesTimers<T: TimerFactory> {
    var timerFactory: T.Type
    private var cancellables = Set<AnyCancellable>()
    
    init(timerFactory: T.Type = RealTimerFactory.self) {
        self.timerFactory = timerFactory
    }
    
    func doSomethingWithTimers() {
        timerFactory.makeTimer()
            .autoconnect()
            .sink { _ in
                print("Tick")
            }
            .store(in: &cancellables)
    }
}

方案2:在协议中封装autoconnect逻辑

如果不想用泛型,直接在协议中提供带autoconnect的方法,避免外部处理存在类型:

protocol TimerFactory {
    static func makeAutoConnectingTimer() -> AnyPublisher<Date, Never>
}

enum FakeTimerFactory: TimerFactory {
    static func makeAutoConnectingTimer() -> AnyPublisher<Date, Never> {
        return FakeTimer().autoconnect().eraseToAnyPublisher()
    }
}

enum RealTimerFactory: TimerFactory {
    static func makeAutoConnectingTimer() -> AnyPublisher<Date, Never> {
        return Timer.publish(every: 1.0, on: .main, in: .default)
            .autoconnect()
            .eraseToAnyPublisher()
    }
}

调整UsesTimers类:

final class UsesTimers {
    var timerFactory: TimerFactory.Type
    private var cancellables = Set<AnyCancellable>()
    
    init(timerFactory: TimerFactory.Type = RealTimerFactory.self) {
        self.timerFactory = timerFactory
    }
    
    func doSomethingWithTimers() {
        timerFactory.makeAutoConnectingTimer()
            .sink { _ in
                print("Tick")
            }
            .store(in: &cancellables)
    }
}

问题2:有没有更好的测试TimerPublisher的方法?

除了自定义Fake定时器,还有几种更高效的方式:

1. 使用Combine自带的TestScheduler

Combine的TestScheduler(需导入XCTest或CombineTesting)可以完全控制时间流逝,无需自定义Fake实现:

import XCTest
import Combine

class TimerTests: XCTestCase {
    func testTimerTicks() {
        let scheduler = TestScheduler(initialClock: 0)
        var ticks = [Date]()
        
        let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .default)
            .autoconnect()
            .subscribe(on: scheduler)
        
        let cancellable = timerPublisher.sink { date in
            ticks.append(date)
        }
        
        // 快进1秒
        scheduler.advance(by: 1.0)
        XCTAssertEqual(ticks.count, 1)
        
        // 再快进2秒
        scheduler.advance(by: 2.0)
        XCTAssertEqual(ticks.count, 3)
        
        cancellable.cancel()
    }
}

这种方式无需修改生产代码,仅在测试时替换调度器即可。

2. 封装定时器协议(灵活手动触发)

封装TimerProtocol,测试时用Fake实现手动触发事件:

protocol TimerProtocol {
    var publisher: AnyPublisher<Date, Never> { get }
    func triggerManually(with date: Date)
}

// 生产环境实现
class RealTimer: TimerProtocol {
    var publisher: AnyPublisher<Date, Never>
    
    init(every: TimeInterval, on runLoop: RunLoop, in mode: RunLoop.Mode) {
        publisher = Timer.publish(every: every, on: runLoop, in: mode)
            .autoconnect()
            .eraseToAnyPublisher()
    }
    
    func triggerManually(with date: Date) {}
}

// 测试环境实现
class FakeTimer: TimerProtocol {
    private let subject = PassthroughSubject<Date, Never>()
    var publisher: AnyPublisher<Date, Never> {
        subject.eraseToAnyPublisher()
    }
    
    func triggerManually(with date: Date = Date()) {
        subject.send(date)
    }
}

生产代码注入TimerProtocol实例,测试时用FakeTimer手动控制事件触发。

3. 使用第三方库

比如Point-Free出品的CombineSchedulers,提供了更友好的调度器抽象,简化测试/生产环境的切换,支持更多场景。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 18:55:10