actor SomActor {
public let myVariable: String
init(){
Task { await self.someFunction }
}
public func someFunction() async {
somePublisher.sink {
self.myVariable = "testMe"
}.store(in: &cancellations)
}
}
now I want to test someFunction was called by checking that myVariable was indeed changed to "testMe".
this code is of-course a simplistic representation to a more complex scenario,
so assume I can't change the code itself.
the main issue is that { self.someFunction } runs a-synchronically. so I can't really wait for it in the test.
please take into consideration:
I hoped there was some XCTest expectation mechanism to help in this case.
( I know expectation predicate could help, but I also don't want to inherit NSObject )
Always interested to see ways of doing stuff in testing.
I am wondering though, could you explain why you would need an unstructured task within an Actor, as it already provides an async context? Is it because its something you want to call in the initializer?
my wish was to make the code a simple as possible. someFunction is actually async function ofCourse (due to actor)
and also inside I am using Combine subscriber...
I fixed the code to be more verbose if it helps
thanks @wadetregaskis
I was worried this would be the only solution.
I decided to make my example a bit more verbose ( so you can give it one more look)
How would you make it testable if I allow changing the code?
would you move someFunction outside the init() so it can be triggered synchronically? (I still left to deal with the async code inside of it...)
would you take the subscriber callback and expose it with public function so it can be tested separately?
may be I should introduce an Interactor and move someFunction there. this way I can mock it and at-least make sure it was called... (but then, how can you change myVariable from within the interactor?)
I like to keep unit tests simple and focus on the effects of the code, not its implementation. I know that's not a popular philosophy currently, but I find that tightly coupling tests to code makes them fragile and slows down development.
Given that, nothing needs to change from the example I posted earlier. That you've introduced a Combine pipeline internally doesn't matter.
That said, if I were pushed to tweak it, I'd probably start with having an explicit signalling mechanism from the code, that the unit test could use to detect pertinent changes. e.g.:
actor SomActor {
public let myVariable: String {
didSet {
#if TESTING_ENABLED
myVariableChange.broadcast()
#endif
}
}
#if TESTING_ENABLED
private var myVariableChange = NSCondition()
#endif
init(){
Task { await self.someFunction }
}
public func someFunction() async {
somePublisher.sink {
self.myVariable = "testMe"
}.store(in: &cancellations)
}
}
Now your unit test can do something like:
let testSubject = SomActor()
XCTAssert(testSubject.myVariableChange.wait(until: Date.now.advanced(by: 1)),
"Timed out waiting for myVariable to change on SomActor.")
The caveat with that approach is you need to be sure that blocking the current thread (in the unit test) won't prevent your test subject from doing its thing asynchronously. In the particular example you provide this isn't an issue, because you're using an actor which is guaranteed to have its own unique, independent isolation domain (which roughly equates to an independent thread).
If it weren't an actor - or some part of the asynchronous code did something like explicitly run on the @MainActor - then you'd probably have to not block the main thread (that's running the unit test), so you'd have to revert to the spin-loop approach with a Task.sleep(for:) in there, for example.
I've changed similar code to return/expose the relevant Task somewhere the unit test can get at it, then you can await task.value to be sure that the task body has completed before continuing.
@wadetregaskis thanks for sharing your solution.
it is indeed less popular (to my view) changing the code this way, although it should work.
there is always a tension between the speed of development and readability.
I will be overwhelmed looking at other's code full of if#endif,
but nonetheless, I am inspired by the innovative thinking you shared here - so thanks a-lot
@wadetregaskis
btw, regarding your first proposed solution:
let timeout = ContinuousClock.now + .seconds(1)
while await testSubject.myVariable != "testMe" {
try! Task.sleep(for: .milliseconds(1))
XCTAssert(.now < timeout)
}
although I also felt it is not Ideal, I gave it a second thought - isn't it what the wait function does behind the scenes anyway?
so why would it be not ideal actually , taking the devil advocate here (;
It's not ideal insofar as it busy-loops, which just wastes a little CPU time. An ideal implementation (all other things being equal) would simply 'sleep' on a signal, waking up only upon that signal. Like what the later implementation does with NSCondition (although NSCondition is not Swift Concurrency-safe or even runloop-compatible, since it blocks the current thread until the condition is signalled, potentially causing deadlock).
But it's not bad. It will work. And for unit tests and the like the CPU usage shouldn't actually matter. It's just not the sort of code I'd ever want to ship to end users.
@wadetregaskis
off topic question:
would you even call this test a Unitest if we actually waiting for some side effect to finish its job...
it feels for me more like integration test.