How to Test Timer with #Published? - swift

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.

Related

Timer fires twice then doesn't reset

I've got a countdown timer that counts down and executes the code twice but then it doesn't reset, instead it continues counting in negative numbers. Can someone tell me why?
var didCount = 4
func startDelayTimer() {
delayTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
self.startDelayCount()
})
}
func startDelayCount() {
delayTime -= 1
timeLbl.text = String(delayTime)
if delayTime <= 3 {
soundPLayer.play()
}
if delayTime == 0 {
delayTimer.invalidate()
doSomething()
}
}
func doSomething() {
doCount += 1
if doCount < didCount {
startDelayTimer()
}
else {
print("done")
}
}
The direct issue is that you reset the timer without remebering to reset delayTime.
But I think there's also an architectural issue, in that you have a murky mix of responsibilities (managing a timer, updating a label, and playing sounds). I'd suggest you extract the timer responsibilities elsewhere.
Perhaps something along these lines:
/// A timer which counts from `initialCount` down to 0, firing the didFire callback on every count
/// After each full countdown, it repeats itself until the repeatLimit is reached.
class RepeatingCountDownTimer {
typealias FiredCallback: () -> Void
typealias FinishedCallback: () -> Void
private var initialCount: Int
private var currentCount: Int // Renamed from old "delayTime"
private var repeatCount = 0 // Renamed from old "doCount"
private let repeatLimit: Int // Renamed from old "didCount"
private var timer: Timer?
private let didFire: FiredCallback
private let didFinish: FinishedCallback
init(
countDownFrom initialCount: Int,
repeatLimit: Int,
didFire: #escaping FiredCallback,
didFinish: #escaping FinishedCallback
) {
self.initialCount = initialCount
self.currentCount = initialCount
self.repeatLimit = repeatLimit
self.didFire = didFire
self.didFinish = didFinish
}
public func start() {
self.currentCount = self.initialCount
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] in
self?.fire()
})
}
private func fire() {
currentCount -= 1
self.didFire(currentCount)
if currentCount == 0 {
repeat()
}
}
private func repeat() {
repeatCount += 1
if repeatCount < repeatLimit {
self.timer?.invalidate()
start()
} else {
finished()
}
}
private func finished() {
self.timer?.invalidate()
self.timer = nil
self.didFinish()
}
}
That's just rough psuedo-code, which will certainly need tweaking. But the idea is to separate timer and state management from the other things you need to do. This should make it easier to debug/develop/test this code, replacing useless names like doSomething with more concretely named events.
The usage might look something like:
let countDownTimer = RepeatingCountDownTimer(
countDownFrom: 4,
repeatLimit: 4,
didFire: { count in
timeLbl.text = String(count)
soundPlayer.play()
},
didFinish: { print("done") }
)
countDownTimer.start()

Unit Testing a Timer?

I want to test that a signal is getting fired every 6 seconds. My code looks like this:
class DataStore {
var clinics: Int { return clinicsSignal.lastDataFired ?? 0 }
let clinicsSignal = Signal<Int>(retainLastData: true)
var timer: Timer?
init() {
}
func load() {
self.clinicsSignal.fire(0)
DispatchQueue.main.async { [weak self] in
self!.timer?.invalidate()
self!.timer = Timer.scheduledTimer(withTimeInterval: 6.0, repeats: true) { [weak self] _ in
self?.clinicsSignal.fire(9)
}
}
}
}
My test code looks like this:
func testRefresh() {
var dataStore: DataStore = DataStore()
dataStore.clinicsSignal.subscribeOnce(with: self) {
print("clinics signal = \($0)")
dataStore.clinicsSignal.fire(0)
}
dataStore.load()
sleep(30)
print("clinics3 = \(dataStore.clinics)")
}
When I sleep for 30 seconds, the timer code doesn't get ran again until after the 30 seconds, therefore it's not getting ran once every 6 seconds like it's supposed to. Any idea on how to test that code in a timer is gettin hit at specific times? Thanks.
The sleep function blocks your thread and timers are associated to threads.
You should use expectation.
func testRefresh() {
var dataStore: DataStore = DataStore()
let expec = expectation(description: "Timer expectation") // create an expectation
dataStore.clinicsSignal.subscribeOnce(with: self) {
print("clinics signal = \($0)")
dataStore.clinicsSignal.fire(0)
expec.fulfill() // tell the expectation that everything's done
}
dataStore.load()
wait(for: [expec], timeout: 7.0) // wait for fulfilling every expectation (in this case only one), timeout must be greater than the timer interval
}

How Can I Unit Test Swift Timer Controller?

I am working a project that will utilize Swift's Timer class. My TimerController class will control a Timer instance by starting, pausing, resuming, and resetting it.
TimerController consists of the following code:
internal final class TimerController {
// MARK: - Properties
private var timer = Timer()
private let timerIntervalInSeconds = TimeInterval(1)
internal private(set) var durationInSeconds: TimeInterval
// MARK: - Initialization
internal init(seconds: Double) {
durationInSeconds = TimeInterval(seconds)
}
// MARK: - Timer Control
// Starts and resumes the timer
internal func startTimer() {
timer = Timer.scheduledTimer(timeInterval: timerIntervalInSeconds, target: self, selector: #selector(handleTimerFire), userInfo: nil, repeats: true)
}
internal func pauseTimer() {
invalidateTimer()
}
internal func resetTimer() {
invalidateTimer()
durationInSeconds = 0
}
// MARK: - Helpers
#objc private func handleTimerFire() {
durationInSeconds += 1
}
private func invalidateTimer() {
timer.invalidate()
}
}
Currently, my TimerControllerTests contains the following code:
class TimerControllerTests: XCTestCase {
func test_TimerController_DurationInSeconds_IsSet() {
let expected: TimeInterval = 60
let controller = TimerController(seconds: 60)
XCTAssertEqual(controller.durationInSeconds, expected, "'durationInSeconds' is not set to correct value.")
}
}
I am able to test that the timer's expected duration is set correctly when initializing an instance of TimerController. However, I don't know where to start testing the rest of TimerController.
I want to ensure that the class successfully handles startTimer(), pauseTimer(), and resetTimer(). I want my unit tests to run as quickly as possible, but I think that I need to actually start, pause, and stop the timer to test that the durationInSeconds property is updated after the appropriate methods are called.
Is it appropriate to actually create the timer in TimerController and call the methods in my unit tests to verify that durationInSeconds has been updated correctly?
I realize that it will slow my unit tests down, but I don't know of another way to appropriately test this class and it's intended actions.
Update
I have been doing some research, and I have found, what I think to be, a solution that seems to get the job done as far as my testing goes. However, I am unsure whether this implementation is sufficient.
I have reimplemented my TimerController as follows:
internal final class TimerController {
// MARK: - Properties
private var timer = Timer()
private let timerIntervalInSeconds = TimeInterval(1)
internal private(set) var durationInSeconds: TimeInterval
internal var isTimerValid: Bool {
return timer.isValid
}
// MARK: - Initialization
internal init(seconds: Double) {
durationInSeconds = TimeInterval(seconds)
}
// MARK: - Timer Control
internal func startTimer(fireCompletion: (() -> Void)?) {
timer = Timer.scheduledTimer(withTimeInterval: timerIntervalInSeconds, repeats: true, block: { [unowned self] _ in
self.durationInSeconds -= 1
fireCompletion?()
})
}
internal func pauseTimer() {
invalidateTimer()
}
internal func resetTimer() {
invalidateTimer()
durationInSeconds = 0
}
// MARK: - Helpers
private func invalidateTimer() {
timer.invalidate()
}
}
Also, my test file has passing tests:
class TimerControllerTests: XCTestCase {
// MARK: - Properties
var timerController: TimerController!
// MARK: - Setup
override func setUp() {
timerController = TimerController(seconds: 1)
}
// MARK: - Teardown
override func tearDown() {
timerController.resetTimer()
super.tearDown()
}
// MARK: - Time
func test_TimerController_DurationInSeconds_IsSet() {
let expected: TimeInterval = 60
let timerController = TimerController(seconds: 60)
XCTAssertEqual(timerController.durationInSeconds, expected, "'durationInSeconds' is not set to correct value.")
}
func test_TimerController_DurationInSeconds_IsZeroAfterTimerIsFinished() {
let numberOfSeconds: TimeInterval = 1
let durationExpectation = expectation(description: "durationExpectation")
timerController = TimerController(seconds: numberOfSeconds)
timerController.startTimer(fireCompletion: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + numberOfSeconds, execute: {
durationExpectation.fulfill()
XCTAssertEqual(0, self.timerController.durationInSeconds, "'durationInSeconds' is not set to correct value.")
})
waitForExpectations(timeout: numberOfSeconds + 1, handler: nil)
}
// MARK: - Timer State
func test_TimerController_TimerIsValidAfterTimerStarts() {
let timerValidityExpectation = expectation(description: "timerValidity")
timerController.startTimer {
timerValidityExpectation.fulfill()
XCTAssertTrue(self.timerController.isTimerValid, "Timer is invalid.")
}
waitForExpectations(timeout: 5, handler: nil)
}
func test_TimerController_TimerIsInvalidAfterTimerIsPaused() {
let timerValidityExpectation = expectation(description: "timerValidity")
timerController.startTimer {
self.timerController.pauseTimer()
timerValidityExpectation.fulfill()
XCTAssertFalse(self.timerController.isTimerValid, "Timer is valid")
}
waitForExpectations(timeout: 5, handler: nil)
}
func test_TimerController_TimerIsInvalidAfterTimerIsReset() {
let timerValidityExpectation = expectation(description: "timerValidity")
timerController.startTimer {
self.timerController.resetTimer()
timerValidityExpectation.fulfill()
XCTAssertFalse(self.timerController.isTimerValid, "Timer is valid")
}
waitForExpectations(timeout: 5, handler: nil)
}
}
The only thing that I can think of to make the tests faster is for me to mock the class and change let timerIntervalInSeconds = TimeInterval(1) to private let timerIntervalInSeconds = TimeInterval(0.1).
Is it overkill to mock the class so that I can use a smaller time interval for testing?
Rather than use a real timer (which would be slow), we can verify calls to a test double.
The challenge is that the code calls a factory method, Timer.scheduledTimer(…). This locks down a dependency. Testing would be easier if the test could provide a mock timer instead.
Usually, a good way to inject a factory is by supplying a closure. We can do this in the initializer, and provide a default value. Then the closure, by default, will make the actual call to the factory method.
In this case, it's a little complicated because the call to Timer.scheduledTimer(…) itself takes a closure:
internal init(seconds: Double,
makeRepeatingTimer: #escaping (TimeInterval, #escaping (TimerProtocol) -> Void) -> TimerProtocol = {
return Timer.scheduledTimer(withTimeInterval: $0, repeats: true, block: $1)
}) {
durationInSeconds = TimeInterval(seconds)
self.makeRepeatingTimer = makeRepeatingTimer
}
Note that I removed all references to Timer except inside the block. Everywhere else uses a newly-defined TimerProtocol.
self.makeRepeatingTimer is a closure property. Call it from startTimer(…).
Now test code can supply a different closure:
class TimerControllerTests: XCTestCase {
var makeRepeatingTimerCallCount = 0
var lastMockTimer: MockTimer?
func testSomething() {
let sut = TimerController(seconds: 12, makeRepeatingTimer: { [unowned self] interval, closure in
self.makeRepeatingTimerCallCount += 1
self.lastMockTimer = MockTimer(interval: interval, closure: closure)
return self.lastMockTimer!
})
// call something on sut
// verify against makeRepeatingTimerCallCount and lastMockTimer
}
}

In Swift, how can I unit test something dependent on a delay implemented with a private NSTimer?

I have a class that uses an NSTimer to buffer a change to one of its stored properties. I'm having trouble unit testing this class without having to expose the private properties and methods. Here is an illustrative version:
import CoreLocation
class BeaconActivity {
private let reaction: BeaconActivity -> ()
private var timer = NSTimer()
private(set) var proximity = CLProximity.Unknown {
didSet {
self.reaction(self)
}
}
init(reaction: BeaconActivity -> ()) {
self.reaction = reaction
}
func startUpdate(proximity: CLProximity) {
self.timer.invalidate()
self.timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: "completeUpdate:", userInfo: proximity.rawValue, repeats: false)
}
dynamic private func completeUpdate(timer: NSTimer) {
let rawValue = timer.userInfo as! Int
self.proximity = CLProximity(rawValue: rawValue)!
}
}
For example, how would I test that reaction that gets passed into the init runs when the proximity property is updated - without having to put a "sleep" in my test code?

How to pass callback functions in Swift

I have a simple class which init method takes an Int and a callback function.
class Timer {
var timer = NSTimer()
var handler: (Int) -> Void
init(duration: Int, handler: (Int) -> Void) {
self.duration = duration
self.handler = handler
self.start()
}
#objc func someMethod() {
self.handler(10)
}
}
Then in the ViewController I have this:
var timer = Timer(duration: 5, handler: displayTimeRemaining)
func displayTimeRemaining(counter: Int) -> Void {
println(counter)
}
This doesn't work, I get the following:
'Int' is not a subtype of 'SecondViewController'
Edit 1: Adding full code.
Timer.swift
import UIKit
class Timer {
lazy var timer = NSTimer()
var handler: (Int) -> Void
let duration: Int
var elapsedTime: Int = 0
init(duration: Int, handler: (Int) -> Void) {
self.duration = duration
self.handler = handler
self.start()
}
func start() {
self.timer = NSTimer.scheduledTimerWithTimeInterval(1.0,
target: self,
selector: Selector("tick"),
userInfo: nil,
repeats: true)
}
func stop() {
timer.invalidate()
}
func tick() {
self.elapsedTime++
self.handler(10)
if self.elapsedTime == self.duration {
self.stop()
}
}
deinit {
self.timer.invalidate()
}
}
SecondViewController.swift
import UIKit
class SecondViewController: UIViewController {
#IBOutlet var cycleCounter: UILabel!
var number = 0
var timer = Timer(duration: 5, handler: displayTimeRemaining)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func btnIncrementCycle_Click(sender: UIButton){
cycleCounter.text = String(++number)
println(number)
}
func displayTimeRemaining(counter: Int) -> Void {
println(counter)
}
}
I just started with Swift so I'm very green. How are you supposed to pass callbacks? I've looked at examples and this should be working I think. Is my class defined incorrectly for the way I'm passing the callback?
Thanks
Ok, now with the full code I was able to replicate your issue. I'm not 100% sure what the cause is but I believe it has something to do with referencing a class method (displayTimeRemaining) before the class was instantiated. Here are a couple of ways around this:
Option 1: Declare the handler method outside of the SecondViewController class:
func displayTimeRemaining(counter: Int) -> Void {
println(counter)
}
class SecondViewController: UIViewController {
// ...
var timer = Timer(duration: 5, handler: displayTimeRemaining)
Option 2: Make displayTimeRemaining into a type method by adding the class keyword to function declaration.
class SecondViewController: UIViewController {
var timer: Timer = Timer(duration: 5, handler: SecondViewController.displayTimeRemaining)
class func displayTimeRemaining(counter: Int) -> Void {
println(counter)
}
Option 3: I believe this will be the most inline with Swift's way of thinking - use a closure:
class SecondViewController: UIViewController {
var timer: Timer = Timer(duration: 5) {
println($0) //using Swift's anonymous method params
}
Simplest way
override func viewDidLoad() {
super.viewDidLoad()
testFunc(index: 2, callback: { str in
print(str)
})
}
func testFunc(index index: Int, callback: (String) -> Void) {
callback("The number is \(index)")
}
Your problem is in the line:
var timer = NSTimer()
You cannot instantiate an NSTimer in this way. It has class methods that generate an object for you. The easiest way to get around the problem in this case is to make the definition lazy, like so:
lazy var timer = NSTimer()
In this way the value for timer won't actually be touched until the start method which sets it up properly. NOTE: There is probably a safer way to do this.