Help using async/await and ViewInspector in a single test
TLDR: Is it possible to have an async XCTest method which also uses ViewInspector (e.g. func test...() async { let exp = view.inspect.inspection }?
More details:
I'm attempting to write tests that call async/await methods which eventually will influence a specific View I'm trying to test. Following the ViewInspector examples, I've been using the Inspection setup, which works great normally. However, whenever I mark a test as async it does not work as can be seen in the contrived examples below. Obviously in these examples I don't call any methods in the test body that actually use await, but I didn't include that as the expectations fails regardless of that fact; I can't even attach async to a test method with a ViewInspector expectation and have it work. Of note, I don't run into this problem if I use a simple XCTExpectation directly. Those can handle both async/await and expectations in a single test method.
Example:
final class Inspection<V> {
let notice = PassthroughSubject<UInt, Never>()
var callbacks = [UInt: (V) -> Void]()
func visit(_ view: V, _ line: UInt) {
if let callback = callbacks.removeValue(forKey: line) {
callback(view)
}
}
}
extension Inspection: InspectionEmissary {}
class TestModel: ObservableObject {
@Published var examples: [String] = []
func addExamples() async throws {
try await Task.sleep(nanoseconds: 1)
let count = self.examples.count
self.examples = self.examples + ["test\(count + 1)", "test\(count + 2)"]
}
}
struct TestView: View {
@StateObject private var testModel = TestModel()
let inspection = Inspection<Self>()
var body: some View {
VStack {
Button("button") {
Task {
try await testModel.addExamples()
}
}.id(1)
ForEach(testModel.examples, id: \.self) {
Text($0).tag("text")
}
}
.onReceive(inspection.notice) {
self.inspection.visit(self, $0)
}
}
}
extension TestView: Inspectable {}
class TestViewTest: XCTestCase {
func test_NoAsyncNoMain() { // Succeeds
testImplementation()
}
@MainActor
func test_NoAsyncMain() { // Succeeds
testImplementation()
}
func test_AsyncMoMain() async { // Fails "Exceeded timeout of 4 seconds, with unfulfilled expectations"
testImplementation()
}
@MainActor
func test_AsyncMain() async { // Fails "Exceeded timeout of 4 seconds, with unfulfilled expectations"
testImplementation()
}
private func testImplementation() {
let testView = TestView()
let expectation1 = testView.inspection.inspect(after: 1) { view in
XCTAssertEqual(view.findAll(where: { try $0.tag() as? String == "text" }).count, 0)
try view.find(viewWithId: 1).button().tap()
}
let expectation2 = testView.inspection.inspect(after: 2) { view in
XCTAssertEqual(view.findAll(where: { try $0.tag() as? String == "text" }).count, 2)
}
ViewHosting.host(view: testView)
wait(for: [expectation1, expectation2], timeout: 4.0, enforceOrder: true)
}
}
While there is no well-thought and finalized approach supported by the library, you can consider going the route suggested by @sisoje in this repo
For anyone interested, I suggest joining a proposed solution discussion in #291 PR