XCTest: Check That a Callback is Not Called - swift

I know how to wait for a callback with expectations in XCT. However, what about testing for the inverse?
Below is an example of a test that I have as part of my tests:
manager.state = .initialized
let exp = expectation(description: "expectation")
manager.login { state, error in
exp.fulfill()
XCTAssert(state == .initialized)
XCTAssertNil(error)
}
waitForExpectations(timeout: 1)
In the next test I'm writing I would like a test to ensure that a callback is not called when my manager object's state is a specific value.

This can be done by setting isInverted = true on the XCTestExpectation object like so:
manager.state = .initialized
let exp = expectation(description: "inverted expectation")
exp.isInverted = true
manager.login { state, error in
exp.fulfill()
}
waitForExpectations(timeout: 1)

Related

Asynchronous DispatchSemaphore.wait(timeout:) Task handle alternative

I'm currently looking for a Task based alternative for my code using DispatchSemaphore:
func checkForEvents() -> Bool
let eventSemaphore = DispatchSemaphore(value: 0)
eventService.onEvent = { _ in
eventSemaphore.signal()
}
let result = eventSemaphore.wait(timeout: .now() + 10)
guard result == .timedOut else {
return true
}
return false
}
I'm trying to detect if any callback events happen in a 10-second time window. This event could happen multiple times, but I only want to know if it occurs once. While the code works fine, I had to convert the enclosing function to async which now shows this warning:
Instance method 'wait' is unavailable from asynchronous contexts; Await a Task handle instead; this is an error in Swift 6`
I'm pretty sure this warning is kind of wrong here since there's no suspension point in this context, but I would still like to know what's the suggested approach in Swift 6 then.
I couldn't find anything about this "await a Task handle" so the closest I've got is this:
func checkForEvents() async -> Bool {
var didEventOccur = false
eventService.onEvent = { _ in
didEventOccur = true
}
try? await Task.sleep(nanoseconds: 10 * 1_000_000_000)
return didEventOccur
}
But I can't figure out a way to stop sleeping early if an event occurs...

Write xctest for function inside an operation queue

I have a function as follows which adds an operation to the operation queue, how do I test the block of code that is being added to the operation queue, I tried using an expectation by passing an analytics observer spy and check if the value is set but it does not seem to work, please point me in the right direction
func firePendingEvent() {
for analyticItem in eventQueue {
eventPriorityQueue.addOperationToQueue (operation: BlockOperation {
self.analyticsObserver?.logEvent(event: analyticItem) // write test to check the event inside this function
}, priority: .normal)
if eventQueue.count > 0 {
eventQueue.remove(at: 0)
}
}
}
The basic idea is that your test has to create an expectation:
let e = expectation(description: "testGetUsers")
And then when your asynchronous method is done, fulfill that expectation:
e.fulfill()
And then, of course, your test has to, after initiating the asynchronous tasks, wait for the expectations:
waitForExpectations(timeout: 10)
But, obviously, you need to know when your function is done. I would suggest giving it an optional completion handler:
func firePendingEvent(completion: (() -> Void)? = nil) {
let group = DispatchGroup()
for analyticItem in eventQueue {
group.enter()
eventPriorityQueue.addOperationToQueue(operation: BlockOperation {
self.analyticsObserver?.logEvent(event: analyticItem)
group.leave()
}, priority: .normal)
}
eventQueue.removeAll()
if let completion = completion {
group.notify(queue: .main, execute: completion)
}
}
(Note, I personally refrain from mutating an array as I iterate through it, so I add all the tasks to the queue and defer the removeAll until I am no longer iterating.)
Anyway, you can now write your test:
func testFirePendingEvents() {
let e = expectation(description: "testGetUsers")
foo.firePendingEvent {
XCAssert(…) // do whatever tests are appropriate
e.fulfill()
}
waitForExpectations(timeout: 10)
}
But, because that completion handler is optional, and defaults to nil, you don't have to change the rest of your code that isn't availing itself of the new completion handler.
Personally, I would add a parameter to that completion handler closure to pass back whatever value you want to test, but there wasn’t enough in the question to know what result you are testing.

How to measure performance of asynchronous method?

I've written a method loadCountries() that performs asynchronous file loading upon calling and then notifies its output that the work is done. I'm trying to measure this method's performance in my tests but I can't figure out how to handle asynchronous operations without callbacks with measure block.
I found out there are startMeasuring() and stopMeasuring() methods that allow to manually set entry and end points for the test. I tried to call the latter in my output mock:
let output = InteractorOutputMock()
output.onDisplay = { _ in
self.stopMeasuring()
}
let interactor = PremiumLocationsChooserInteractor()
interactor.output = output
measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) {
self.startMeasuring()
interactor.loadCountries()
}
But the code still finishes in 0 secs. How should I approach this issue?
UPDATE:
I tried using expectations as well, but ran into problem. I can neither call it inside the measure block nor outside of it. The former looks like the following and causes the code to wait in the first iteration of measure:
let outputCalledExpectation = XCTestExpectation(description: "Output hasn't been called")
outputCalledExpectation.expectedFulfillmentCount = 10 // need to fullfill it 10 times since this is how much measure block is called
let output = InteractorOutputMock()
output.onDisplay = { _ in
self.stopMeasuring() // called only once
outputCalledExpectation.fulfill()
}
let interactor = PremiumLocationsChooserInteractor()
interactor.output = output
measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) {
startMeasuring()
interactor.loadCountries()
wait(for: [outputCalledExpectation], timeout: 5.0) // then stack here
}
And if I try to move the wait method outside of the block, I get exception -stopMeasuring is only supported from a block passed to -measure...Block:
measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) {
startMeasuring()
interactor.loadCountries()
}
wait(for: [outputCalledExpectation], timeout: 5.0) // get exception here
I use this extension for measuring, works like a charm for me.
extension XCTestCase {
func measureAsync(
timeout: TimeInterval = 2.0,
for block: #escaping () async throws -> Void,
file: StaticString = #file,
line: UInt = #line
) {
measureMetrics(
[.wallClockTime],
automaticallyStartMeasuring: true
) {
let expectation = expectation(description: "finished")
Task { #MainActor in
do {
try await block()
expectation.fulfill()
} catch {
XCTFail(error.localizedDescription, file: file, line: line)
expectation.fulfill()
}
}
wait(for: [expectation], timeout: timeout)
}
}
}

AKAmplitudeTracker amplitude getting 0.0 using audioKit

I want to get the volume of AKAmplitudeTracker but getting -inf what is wrong with me please help out.
AKAudioFile.cleanTempDirectory()
AKSettings.audioInputEnabled = true
AKSettings.bufferLength = .medium
AKSettings.defaultToSpeaker = true
AKSettings.playbackWhileMuted = true
AKSettings.enableRouteChangeHandling = true
AKSettings.enableCategoryChangeHandling = true
AKSettings.enableLogging = true
do {
try AKSettings.setSession(category: .playAndRecord, with: .allowBluetoothA2DP)
} catch {
print("error \(error.localizedDescription)")
}
microphone = AKMicrophone()!
tracker = AKAmplitudeTracker(microphone)
booster = AKBooster(tracker, gain: 0)
AudioKit.output = booster
try AudioKit.start()
=================
extension AKAmplitudeTracker {
var volume: Decibel {
return 20.0 * log10(amplitude)
}
}
=================
OutPut print(tracker. amplitude)
0.0
Had a quick look, seems that you followed the basic setup, you do seem to fail to trace the data generated in time correctly! Amplitude data is provided during the time period for the computation that is taken from the microphone, so to look at what it looks like in timeline you can use a timer, as such:
func reset() throws {
do {
self.timer.invalidate()
self.timer = nil
} catch {
throw error
}
}
func microphoneTracker() {
guard self.timer == nil else { return }
self.watcher()
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
log.info(self.akMicrophoneAmplitudeTracker.amplitude)
}
self.timer = timer
}
Change the withTimeInterval to how frequently you want to check the amplitude.
I think it's quite readable what I put there for you, but I'll break it down in a few words:
Keep a reference for the AKAmplitudeTracker in a property, here I've named it akMicrophoneAmplitudeTracker
Keep a reference for your timed event, that will check the amplitude value during a period
Compute the data in the closure body, the property holding value is .amplitude
The computation in the example is a logger that prints .amplitude
As required, use the .invalidate method to stop the timer
A few other things you may want to double-check on your code is to make sure that the tracker is part of the signal chain, as that's an AVAudioEngine engine requirement; I've also noticed in some other people's code a call for the method .start in the AKAmplitudeTracker, as follows:
akMicrophoneAmplitudeTracker.start()
To finish, have in mind that if you are testing it through Simulator, look at the microphone settings of your host-machine and expect amplitudes that might be different then the actual hardware.

What if XCTestExpectation is unexpected

I'm writing an XCTest unit test in Swift.
The idea is that a callback mustn't be called in a certain case.
So what I do, is
func testThatCallbackIsNotFired() {
let expectation = expectationWithDescription("A callback is fired")
// configure an async operation
asyncOperation.run() { (_) -> () in
expectation.fulfill() // if this happens, the test must fail
}
waitForExpectationsWithTimeout(1) { (error: NSError?) -> Void in
// here I expect error to be not nil,
// which would signalize that expectation is not fulfilled,
// which is what I expect, because callback mustn't be called
XCTAssert(error != nil, "A callback mustn't be fired")
}
}
When the callback is called, everything works fine: it fails with a message "A callback mustn't be fired" which is exactly what I need.
But if expectation hasn't been fulfilled, it fails and says
Asynchronous wait failed: Exceeded timeout of 1 seconds, with unfulfilled expectations: "Callback is fired".
Since a not fulfilled expectation is what I need, I don't want to have a failed test.
Do you have any suggestions what can I do to avoid this? Or, maybe, I can reach my goal in a different way? Thanks.
Use isInverted like in this post https://www.swiftbysundell.com/posts/unit-testing-asynchronous-swift-code
class DebouncerTests: XCTestCase {
func testPreviousClosureCancelled() {
let debouncer = Debouncer(delay: 0.25)
// Expectation for the closure we'e expecting to be cancelled
let cancelExpectation = expectation(description: "Cancel")
cancelExpectation.isInverted = true
// Expectation for the closure we're expecting to be completed
let completedExpectation = expectation(description: "Completed")
debouncer.schedule {
cancelExpectation.fulfill()
}
// When we schedule a new closure, the previous one should be cancelled
debouncer.schedule {
completedExpectation.fulfill()
}
// We add an extra 0.05 seconds to reduce the risk for flakiness
waitForExpectations(timeout: 0.3, handler: nil)
}
}
I had this same problem, and I am annoyed that you can't use a handler to override the timeout fail of waitForExpectationsWithTimeout. Here is how I solved it (Swift 2 syntax):
func testThatCallbackIsNotFired() {
expectationForPredicate(NSPredicate{(_, _) in
struct Holder {static let startTime = CACurrentMediaTime()}
if checkSomehowThatCallbackFired() {
XCTFail("Callback fired when it shouldn't have.")
return true
}
return Holder.startTime.distanceTo(CACurrentMediaTime()) > 1.0 // or however long you want to wait
}, evaluatedWithObject: self, handler: nil)
waitForExpectationsWithTimeout(2.0 /*longer than wait time above*/, handler: nil)
}