How to Test Timer with @Published?

Solution for How to Test Timer with @Published?
is Given Below:

I created a view which uses a ObservableObject which used an Timer to update seconds which are an @Published property.

class TimerService: ObservableObject {

    @Published var seconds: Int

    var timer: Timer?

    convenience init() {
        self.init(0)
    }
    
    init(_ seconds: Int){
        self.seconds = seconds
    }

    func start() {
      ...
      self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in 
      self.seconds += 1 }
      self.timer?.fire()
    }

    func stop() {...}

    func reset() {...}
}

To test this logic I tried to subscribe to the seconds var. The problem is that the .sink method only trigger once and never again, even when it should.

class WorkTrackerTests: XCTestCase {
    var timerService: TimerService!

    override func setUpWithError() throws {
        super.setUp()
        timerService = TimerService()
    }

    override func tearDownWithError() throws {
        super.tearDown()
        timerService = nil
    }

    func test_start_timer() throws {
        var countingArray: [Int] = []
        
        timerService.start()
        timerService.$seconds.sink(receiveValue: { value -> Void in
            print(value) // 1 (called once with this value)
            countingArray.append(value)
            
        })
        timerService.stop()
        
        for index in 0...countingArray.count-1 {
            if(index>0) {
                XCTAssertTrue(countingArray[index] - 1 == countingArray[index-1])
                
            }
        }
    }

}

Is there something I did wrong or is the SwiftUI @Published Wrapper not capable of being subscribed by something else than SwiftUI itself?

I’ll start by repeating what I said already in comments. There is no need to test Apple’s code. Don’t test Timer. You know what it does. Test your code.

As for your actual example test harness, it is flawed from top to bottom. A sink without a store will indeed get only one value, if it gets any at all. But the issue runs even deeper, as you are acting like your code will magically stop and wait for the timer to finish. It won’t. You are saying stop immediately after saying start, so the timer never even runs. Asynchronous input requires asynchronous testing. You would need an expectation and a waiter.

But it is very unclear why you are subscribing to the publisher at all. What are you trying to find out? The only question of interest, it seems, is whether you are incrementing your variable each time the timer fires. And you can test that without subscribing to a publisher — and, as I said, without a Timer.

So much for the repetition. Now let’s demonstrate. Let’s start with the code you’ve actually shown, the only code that has content:

class TimerService: ObservableObject {
    @Published var seconds: Int
    var timer: Timer?
    convenience init() {
        self.init(0)
    }
    init(_ seconds: Int){
        self.seconds = seconds
    }
    func start() {
      self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
          self.seconds += 1
      }
      self.timer?.fire()
    }
}

Now look at all the commands you send to the Timer and encapsulate them in a Timer subclass:

class TimerMock : Timer {
    var block : ((Timer) -> Void)?
    convenience init(block: (@escaping (Timer) -> Void)) {
        self.init()
        self.block = block
    }
    override class func scheduledTimer(withTimeInterval interval: TimeInterval,
            repeats: Bool,
            block: @escaping (Timer) -> Void) -> Timer {
        return TimerMock(block:block)
    }
    override func fire() {
        self.block?(self)
    }
}

Now make your TimerService a generic so that we can inject TimerMock when testing:

class TimerService<T:Timer>: ObservableObject {
    @Published var seconds: Int
    var timer: Timer?
    convenience init() {
        self.init(0)
    }
    init(_ seconds: Int){
        self.seconds = seconds
    }
    func start() {
      self.timer = T.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
          self.seconds += 1
      }
      self.timer?.fire()
    }
}

So now we can test your logic without bothering to run a timer:

import XCTest
@testable import TestingTimer // whatever the name of your module is
class TestingTimerTests: XCTestCase {
    func testExample() throws {
        let timerService = TimerService<TimerMock>()
        timerService.start()
        if let timer = timerService.timer {
            timer.fire()
            timer.fire()
            timer.fire()
        }
        XCTAssertEqual(timerService.seconds,4)
    }
}

None of your other code needs to change; you can go on using TimerService as before. I can think of other ways to do this, but they would all involve dependency injection where in “real life” you use a Timer but when testing you use a TimerMock.