Swift: Testing async functions nested in timers? - swift

I'm new to swift, and facing an issue with testing asynchronous swift code nested within a timer.
I have a swift .scheduledTimer that sends data to an API every 5 minutes in swift via URLSession.shared.upload
In order for the asynchronous call to work within .scheduledTimer I've wrapped it in Task , like so:
self.timerProvider.scheduledTimer(withTimeInterval: timeBetweenAPISendsInSeconds, repeats: true) { _ in
Task {
await URLSession.shared.upload(for: request, from: encodedSessionBatch)
}
}
The problem I have is with unit testing. Specifically:
func test_scheduledTimer_sendsSessionBatchToAPI() async throws {
MockTimer.currentTimer.fire()
XCTAssertEqual(mockApiDispatcher.sentToAPICount, 1)
}
That "sentToAPICount" won't get picked up, because the test fires before the async operation completes. This normally isn't a problem with async tests, I either:
Stick 'async/await' in the test function. Can't do that in this instance(?), because the async call is nested within a timer
Use expectation.fulfill()...which I'm guessing requires a callback to use effectively, which this code doesn't have?
As mentioned, I'm relatively new to swift, and eager for ideas: what's the standard way of asynchronous tests nested in timers?

Related

Best practice with asynchronous functions Swift & Combine

I'm converting my Swift app to use Combine as well as async/await and I'm trying to understand what's the best way to handle interactions between asynchronous functions and the main thread.
Here's an asynchronous function that loads a user:
class AccountManager {
static func fetchOrLoadUser() async throws -> AppUser {
if let user = AppUser.current.value {
return user
}
let syncUser = try await loadUser()
let user = try AppUser(syncUser: syncUser)
AppUser.current.value = user // [warning]: "Publishing changes from background threads is not allowed"
return user
}
}
And a class:
class AppUser {
static var current = CurrentValueSubject<AppUser?,Never>(nil)
// ...
}
Note: I chose to use CurrentValueSubject because it allows me to both (1) read this value synchronously whenever I need it and (2) subscribe for changes.
Now, on the line marked above I get the error Publishing changes from background threads is not allowed, which I understand. I see different ways to solve this issue:
1. Mark whole AccountManager class as #MainActor
Since most of the work done in asynchronous functions is to wait for network results, I'm wondering if there is an issue with simply running everything on the main thread. Would that cause performance issues or not?
2. Englobe error line in DispatchQueue.main.sync
Is that a reasonable solution, or would that cause threading problems like deadlocks?
3. Use DispatchGroup with enter(), leave() and wait()
Like in this answer. Is there a difference at all with solution #2? Because this solution needs more lines of code so I'd rather not use it if possible —I prefer clean code.
You can wrap the call in an await MainActor.run { } block. I think this is the most Swifty way of doing that.
You should not use Dispatch mechanism while using Swift Concurrency, event though I think DispatchQueue.main.async { } is safe to use here.
The #MainActor attribute is safe but shouldn’t be used on anObservableObject and could potentially slow down the UI if CPU-bound code is run in a method of the annotated type.

GCD serial queue like approach using swift async/await api?

I am adopting the new async/await Swift API. Things work great.
Currently in my apps I am using GCD serial queues as a pipeline to enforce tasks to happen serially.
For manipulating data of some critical models in the app I use a serial queue accessible from anywhere in the app:
let modelQueue = DispatchQueue(label: "com.myapp.updatemodelqueue")
Anytime a model needs to modify some model data I dispatch to that queue:
modelQueue.async {
// ... model updates
}
With the new async/await changes I am making I still want to force model actual updates to happen serially. So, for example, when I import new model data from the server I want to make sure they happen serially.
For example, I may have a call in my model that looks like this:
func updateCarModel() async {
let data = await getModelFromServer()
modelQueue.async {
// update model
}
}
Writing that function using that pattern however would not wait for the model update changes because of the modelQueue.async. I do not want to use modelQueue.sync to avoid deadlocks.
So then after watching WWDC videos and looking at documentation I implemented this way, leveraging withCheckedContinuation:
func updateCarModel() async {
let data = await getModelFromServer()
await withCheckedContinuation({ continuation in
modelQueue.async {
// update model
continuation.resume()
}
})
}
However, to my understanding withCheckedContinuation is really meant to allow us to incrementally transition to fully adopt the new async/await Swift API. So, it does not seem to be what I should use as a final approach.
I then looked into actor, but I am not sure how that would allow me to serialize any model work I want to serialize around the app like I did with a static queue like shown above.
So, how can I enforce my model around the app to do model updates serially like I used to while also fully adopting the new await/async swift API without using withCheckedContinuation?
By making the model an actor, Swift synchronizes access to it' shared mutable state. If the model is written like this:
actor Model {
var data: Data
func updateModel(newData: Data) {
data = newData
}
}
The updateModel function here is synchronous, it's execution is uninterrupted after it's invoked. Because Model is an actor, Swift restricts you to treat it as if you are calling an asynchronous funtion from the outside. You'd have to await, which results in suspension of your active thread.
If in case you'd want to make updateModel async, the code within will always be synchronous unless if you explicitly suspend it by calling await. The order of execution of multiple updateModel calls is not very deterministic. As far as you don't suspend within the updateModel block, it is sure that they execute serially. In such case, there is no use making the updateModel async.
If your update model code is synchronous you can make your model actor type to synchronize access. Actors in swift behave similar to serial DispatchQueue, they perform only one task at a time in the order of submission. However, current swift actors are re-entrant, which means if you are calling any async method actor suspends the current task until the async function completes and proceeds to process other submitted tasks.
If your update code is asynchronous, using an actor might introduce data race. To avoid this, you can wait for non-reentrant actor support in swift. Or you can try this workaround TaskQueue I have created to synchronize between asynchronous tasks or use other synchronization mechanisms that are also created by me.

is there a library similar to Reentrant Lock in Java for Swift?

I am currently working on a function that can be used by multiple threads. The issue is that the function needs to complete first and store the result in the cache. In the meantime, other threads could be calling this function and I would need them to wait until is completed. We were able to accomplish this on Java using Reentrant Lock is there a similar library in swift? I saw that NSRecursiveLock approaches what we are trying to do, however, we want to keep it with swift only. I have also been seeing multiple articles such as this one that talks about using GCD, however, I believe this is for something similar but different: https://medium.com/#prasanna.aithal/multi-threading-in-ios-using-swift-82f3601f171c
Thank you in advance.
Recursion with locking is always a bit of a pain point. A clean solution would be to refactor your function that requires the lock into an external API that acquires the lock and forwards to an internal API that doesn't. Internally don't call the external API.
A simple example might be something like this (this is almost Swift code - parameters and actual work implementations need to be filled in)
extension DispatchSemaphore
{
func withLock<R>(_ block: () throws -> R) rethrows -> R
{
wait()
defer { signal() }
return try block()
}
}
let myLock = DispatchSemaphore(value: 1)
func recursiveLockingFunction(parameters)
{
func nonLockingFunc(parameters) {
if /* some terminating case goes here */ {
// Do the terminating case
return
}
// Do whatever you need to do to handle the partial problem and
// and reduce the parameters
nonLockingFunc(reducedParameters)
}
myLock.withLock { nonLockingFunc(parameters) }
}
Whether this will work for you depends on your design, but should work if the only problem is that the function you want to lock is recursive. And it only uses GCD (DispatchSemaphore) to achieve it.

How can I freeze Kotlin objects created in Swift?

I’m using Kotlin-Native with native-mt coroutine support and the Ktor library. I have several suspended functions that take in an object built using a builder pattern. I understand I need to call the suspended function on the main/ui thread. However, I can’t guarantee that the builder objects will be created on that thread. My understanding is they would need to be frozen before sending them to the main thread to be called with the suspended function. Is that correct?
For instance, this would fail because the query object hasn’t been frozen:
func loadData() {
DispatchQueue.global(qos: .background).async {
let query = CustomerQuery().emails(value: ["customer#gmail.com"])
self.fetchCustomersAndDoSomething(query: query)
}
}
func fetchCustomersAndDoSomething(query: CustomerQuery) {
DispatchQueue.main.async {
self.mylibrary.getCustomers(query: query) { response, err in
// do something with response
}
}
}
If that’s true, am I correct that I would need to add a method to every such object in order to ‘freeze’ it, since the freeze() Kotlin function from Freezing.kt doesn’t seem to be accessible from the Swift code importing my library? This is further complicated by the fact that freezing only applies to the iOS code, as the Android code doesn't need it.
Is there a simpler way to pass in Kotlin objects created by Swift to a suspended function, without requiring that those objects be created on the main thread?
In the Kotlin/Native world, whenever you are sharing objects between threads you have to make sure they are frozen (immutable), if you are not planning on making them #ThreadLocal. Android is an exception, since JVM is not that strict, and let's you share mutable objects between threads.
You have two options:
Either expose a freeze() function and use that
freeze() every incoming object in your shared code
Also if you don't freeze, probably you'll bump into IncorrectDereferenceException, which means you are trying to share mutable/non-frozen state
You don't freeze Swift classes. If CustomerQuery is a Kotlin class, you would need to freeze that.
However, you only need to call suspend functions on the main thread if you rely on the auto-generated Objc interface from the Kotlin compiler. We generally recommend not doing that because you can't control the lifecycle, but that's a whole different discussion.

How to assert an error is thrown async when testing?

We can test thrown errors with XCTAssertThrowsError. Async things can be tested with expectation. I have some method which dispatch work to a background thread and can at some point throw an error.
Is it possible to expect an error be thrown somewhere in the future? I need to combine expectation and XCTAssertThrowsError I think, but I do not know how.
Reproduction project: https://github.com/Jasperav/ThrowingAsyncError. Just clone the project and run the tests, one of them will fail. I made a class which will crash after a few seconds after it has been allocated. I want to make sure it keeps crashing after a few seconds, so I want a test case for it.
You could fulfill an expectation in the catch block of a call that is expected to fail.
func testFailingAsyncCode() async throws {
let expectation = expectation(description: "expect call to throw error")
let dataFetcher = DataFetcher()
do {
// This call is expected to fail
let data = try await dataFetcher.fetchData(withRequest: request, validStatusCodes: [200])
} catch {
// Expectation is fulfilled when call fails
expectation.fulfill()
}
wait(for: [expectation], timeout: 3)
}
I took the sample code from David B.'s answer and changed it, because expectations are not needed when the unit test method is annotated with async.
func testFailingAsyncCode() async { // async is important here
let dataFetcher = DataFetcher()
var didFailWithError: Error?
do {
// This call is expected to fail
_ = try await dataFetcher.fetchData(withRequest: request, validStatusCodes: [200])
} catch {
didFailWithError = error
// Here you could do more assertions with the non-nil error object
}
XCTAssertNotNil(didFailWithError)
}
I took a look at the reproduction project to see what you were trying to accomplish here...
To my understanding:
XCTAssertThrowsError are assertions that takes in a block that can throw. They just happen to assert that an error is thrown in a synchronous block when it's done running.
XCTestExpectation are classes that keep track of whether or not requested conditions are met. They are for keeping track of asynchronous code behavior objects/references need to be kept and checked later.
What you seem to be trying to do is make something like XCTestExpectation work the same way XCTAssertThrowsError does, as in make an synchronous assertion that an asynchronous block will throw. It won't work quite that way because of how the code runs and returns.
The asynchronous code you refer to does not throw (timer initializer). As far as I know, there aren't any asynchronous blocks that can throw. Perhaps the question you should be asking is how can we make a synchronous operation choose to run synchronously sometimes, but also asynchronously when it feels like...
Alternatively for some additional complexity in every class you would like to test I've made a solution with what is almost bare minimum to make this easily testable and portable...
https://github.com/Jasperav/ThrowingAsyncError/pull/1/files
May I ask why you would ever want to do something like this?