Search code examples
iosswiftxcodeunit-testingasynchronous

Xcode and Swift unit testing View asynchronously using ViewInspector times out


I am using Xcode 15.0.1 to build a swift package, this is not a project. I am getting a failure when the wait in the test below times out and it does not run the closure on the test.on(\.didAppear) line. FYI I have this working in other tests in my package.

In my Package.swift file I have the following.

    // swift-tools-version: 5.9
    // The swift-tools-version declares the minimum version of Swift required to build this package.
    
    import PackageDescription
    
    let package = Package(
        name: "MyLibrary",
        platforms: [
            .iOS(.v16)
        ],
        products: [
            // Products define the executables and libraries a package produces, and make them visible to other packages.
            .library(name: "MyLibrary", targets: ["MyLibrary"])
        ],
        dependencies: [
            // Dependencies declare other packages that this package depends on.
            // .package(url: /* package url */, from: "1.0.0"),
            .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
            .package(url: "https://github.com/realm/SwiftLint.git", from: "0.53.0"),
            .package(url: "https://github.com/nalexn/ViewInspector.git", from: "0.9.8")
        ],
        targets: [
            // Targets are the basic building blocks of a package. A target can define a module or a test suite.
            // Targets can depend on other targets in this package, and on products in packages this package depends on.
            .target(
                name: "MyLibrary",
                dependencies: [],
                resources: [
                    .process("Media.xcassets")
                ],
                plugins: [
                    .plugin(name: "SwiftLintPlugin", package: "SwiftLint")
                ]
            ),
            .testTarget(
                name: "MyLibraryTests",
                dependencies: ["MyLibrary", "ViewInspector"])
        ]
    )

Here is a code that fails to run. It is the same basic structure as the code in my package.

import SwiftUI
import ViewInspector
import XCTest
@testable import MyLibrary

final class MyTests: XCTestCase {
    
    struct TestView: View {
        @Binding var text: String
        @State var display: String = ""

        internal var didAppear: ((Self) -> Void)?

        init(text: String) {
            self._text = Binding<String>.init(wrappedValue: text)
        }

        var body: some View {
            HStack {
                Text("Hello")
                Text(display)
            }
            .onChange(of: text, perform: { value in
                self.display = value
            })
        }
    }

    func testTest() throws {
        var test = TestView(text: "")

        let exp = test.on(\.didAppear) { view in
            try view.hStack().callOnChange(newValue: "World")
            XCTAssertNotNil(try view.hStack().find(text: "World"))
        }

        ViewHosting.host(view: test)
        wait(for: [exp], timeout: 1.0) // <- Fails here with a timeout
    }
 }

I use the onChange in the test above to use the same update mechanism as in my code.


Solution

  • I found the answer to my question. There is a line missing in the var body: some View variable. It should include .onAppear { self.didAppear?(self) }. Therefore the TestView should be:

        struct TestView: View {
            @Binding var text: String
            @State var display: String = ""
    
            internal var didAppear: ((Self) -> Void)?
    
            init(text: String) {
                self._text = Binding<String>.init(wrappedValue: text)
            }
    
            var body: some View {
                HStack {
                    Text("Hello")
                    Text(display)
                }
                .onChange(of: text, perform: { value in
                    self.display = value
                })
                .onAppear { self.didAppear?(self) } // <- this was missing
            }
        }
    

    Otherwise exp is never set and the wait function has nothing to "wait" for.