As you're injecting the depencency via a protocol, you're in a very good position for providing a Fake for that protocol, a fake which you have full control from the unit tests:
class ServiceFake: ServiceType {
var doSomethingReply: (CheckedContinuation<Void, Error>) -> Void = { _ in }
func doSomething() async {
// this creates a continuation, but does nothing with it
// as it waits for the owners to instruct how to proceed
await withCheckedContinuation { doSomethingReply($0) }
}
}
With the above in place, your unit tests are in full control: they know when/if doSomething
was called, and can instruct how the function should respond.
final class ViewModelTests: XCTestCase {
func test_viewModelIsLoadingWhileDoSomethingSuspends() {
let serviceFake = ServiceFake()
let viewModel = ViewModel(service: serviceFake)
XCTAssertEquals(viewModel.state, .notLoaded)
let expectation = XCTestExpectation(description: "doSomething() was called")
// just fulfilling the expectation, because we ignore the continuation
// the execution of `load()` will not pass the `doSomething()` call
serviceFake.doSomethingReply = { _ in
expectation.fulfill()
}
Task {
viewModel.load()
}
wait(for: [expectation], timeout: 0.1)
XCTAssertEqual(viewModel.state, .loading)
}
}
The above test makes sure doSomething()
is called, as you likely don't want to validate the view model state until you're sure the execution of load()
reached the expected place - afterall, load()
is called on a different thread, so we need an expectation to make sure the test properly waits until the thread execution reaches the expected point.
The above technique is very similar to a mock/stub, where the implementation is replaced with a unit-test provided one. You could even go further, and just have an async closure instead of a continuation-based one:
class ServiceFake: ServiceType {
var doSomethingReply: () async -> Void = { }
func doSomething() async {
doSomethingReply()
}
}
, and while this would give even greater control in the unit tests, it also pushes the burden of creating the continuations on those unit tests.