Why this test case hangs when run on a simulator? - swift

Consider this simple test case
func test_Example() async {
let exp = expectation(description: "It's the expectation")
Task {
print("Task says: Hello!")
exp.fulfill()
}
wait(for: [exp], timeout: 600)
}
When I run this on an actual device, the test passes. However, when I run it on a simulator, it hangs and never completes.
Removing the async keyword from the function declaration solves the issue and the test passes on a simulator as well.
My actual use case / tests require the test to be async, so removing it is not really an option.

Solution: Change wait(for:) to await waitForExpectations(timeout:)
I don't know why it works, but probably it has something to do with the limited amount of threads available in a simulator.
waitForExpectations(timeout:) is marked with #MainActor and wait(for:) is not.

The mistake here doesn't really have anything to do with testing; it seems to have to do with what async/await is.
In your "simple test case", you have mistakenly added async to a method that is not async — that is, it doesn't make any await calls to an async method. This has nothing to do with tests, really; it's just a wrong thing to do in general, even though the compiler does not help you by warning you about it.
On the contrary, the chief purpose of a Task block is usually to let you call an async method in a context that is not async. That is why, when you take away the async designation, everything goes fine; you are behaving a lot more correctly.
However, the example still suffers from the problem that you aren't doing anything async even inside the Task. A more appropriate use of a Task would look more like this:
func test_Example() {
let exp = expectation(description: "It's the expectation")
Task {
try await Task.sleep(for: .seconds(3))
print("Task says: Hello!")
exp.fulfill()
}
wait(for: [exp], timeout: 4)
}
I have deliberately configured my numbers so that we are actually testing something here. This test passes, but if you change the 4 in the last line to 2, it fails — thus proving that we really are testing whether the Task finished in the given time, and failing if it doesn't.
If you're going to mark a test as async, then you would not use a Task inside it; you are already in a Task! But if we do that, then there is no need to wait for any expectations at all; the word await already waits:
func test_Example() async throws {
print("Task starts")
try await Task.sleep(for: .seconds(3))
print("Task says: Hello!") // three seconds later
}
But at this point we are no longer testing anything. And that's sort of the point; there isn't anything about your original async example that is async, so it remains unclear why we here in the first place.

Related

Swift: Testing async functions nested in timers?

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?

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.

How to use URLSession in MainActor

I have a ViewModel that is isolated with #MainActor and I assume that every part of this ViewModel class should run on the main actor (main thread), right?
So the question is, what about the async call of URLSession.shared.data?
Does it also run on main thread? Isn't that a bad approach?
#MainActor
class ViewModel {
#Published var name: String = ""
func fetchName() async {
guard let (data, _) = try? await URLSession.shared.data(from: URL(string: "http://....")!),
let response = try? JSONDecoder().decode(Response.self, from: data) else {
return
}
self.name = response.name
}
}
Does it also run on main thread
No. That's the whole point of saying await. At that moment, the system can switch contexts to a background thread without you knowing or caring. Moreover, at await your code pauses without blocking — meaning that while you're waiting for the download, the main thread is free to do other work, the user can interact with your app, etc. Don't worry be happy.
The issue is not the URLSession code that you await. That runs on a separate thread managed by URLSession. Whenever you see await, that means that the current execution is suspended while the asynchronous task runs, and that the current actor is free to run other code. All is well.
The only potential concern is code that runs synchronously on the main actor (i.e., everything besides what we await). In this case, the only relevant portion is the code after the await, inside what is called the “continuation”.
In this case, the continuation consists of the decode of the JSON and the updating of the property. That is modest and you are generally fine running that on the main actor.
But if the continuation consisted of anything anything more substantial (e.g. decoding many megabytes of JSON or decoding an image or the like), then you would want to move that off the main actor. But in this case, you are fine.
For more information, see WWDC 2021 video Swift concurrency: Behind the scenes.

Why does a Task within a #MainActor not block the UI?

Today I refactored a ViewModel for a SwiftUI view to structured concurrency. It fires a network request and when the request comes back, updates a #Published property to update the UI. Since I use a Task to perform the network request, I have to get back to the MainActor to update my property, and I was exploring different ways to do that. One straightforward way was to use MainActor.run inside my Task, which works just fine. I then tried to use #MainActor, and don't quite understand the behaviour here.
A bit simplified, my ViewModel would look somewhat like this:
class ContentViewModel: ObservableObject {
#Published var showLoadingIndicator = false
#MainActor func reload() {
showLoadingIndicator = true
Task {
try await doNetworkRequest()
showLoadingIndicator = false
}
}
#MainActor func someOtherMethod() {
// does UI work
}
}
I would have expected this to not work properly.
First, I expected SwiftUI to complain that showLoadingIndicator = false happens off the main thread. It didn't. So I put in a breakpoint, and it seems even the Task within a #MainActor is run on the main thread. Why that is is maybe a question for another day, I think I haven't quite figured out Task yet. For now, let's accept this.
So then I would have expected the UI to be blocked during my networkRequest - after all, it is run on the main thread. But this is not the case either. The network request runs, and the UI stays responsive during that. Even a call to another method on the main actor (e.g. someOtherMethod) works completely fine.
Even running something like Task.sleep() within doNetworkRequest will STILL work completely fine. This is great, but I would like to understand why.
My questions:
a) Am I right in assuming a Task within a MainActor does not block the UI? Why?
b) Is this a sensible approach, or can I run into trouble by using #MainActor for dispatching asynchronous work like this?
await is a yield point in Swift. It's where the current Task releases the queue and allows something else to run. So at this line:
try await doNetworkRequest()
your Task will let go of the main queue, and let something else be scheduled. It won't block the queue waiting for it to finish.
This means that after the await returns, it's possible that other code has been run by the main actor, so you can't trust the values of properties or other preconditions you've cached before the await.
Currently there's no simple, built-in way to say "block this actor until this finishes." Actors are reentrant.

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?