ViewInspector icon indicating copy to clipboard operation
ViewInspector copied to clipboard

Help testing update by async func

Open atsu0127 opened this issue 3 years ago • 2 comments

I wrote a View and test that is updated by an asynchronous function in SwiftUI. But the test failed. I would like to know how I should write tests in this case.

View

import SwiftUI

struct ContentView: View {
    internal var didAppear: ((Self) -> Void)?
    @State var count = 0
    
    var body: some View {
        NavigationView {
            VStack {
                Text(count.description)
                    .tag("count")
            }
            .onAppear { self.didAppear?(self) }
            .navigationTitle("TestApp")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing){
                    Button {
                        Task {
                            await self.update()
                        }
                    } label: {
                        Label("update", systemImage: "arrow.clockwise")
                    }
                    .tag("updateButton")
                }
            }
        }
    }
    
    func update() async {
        try? await Task.sleep(until: .now + .seconds(1), clock: .continuous)
        self.count += 3
    }
}

Test

import XCTest
import ViewInspector
@testable import TestApp

extension ContentView: Inspectable {}

final class TestAppTests: XCTestCase {
    
    func testSample() throws {
        let startCount = "0"
        let incCount = "3"
        var sut = ContentView()
        
        let exp = sut.on(\.didAppear) { view in
            var count = try view.find(viewWithTag: "count").text().string()
            XCTAssertEqual(count, startCount)
            
            try view.find(viewWithTag: "updateButton").button().tap()
            
            count = try view.find(viewWithTag: "count").text().string()
            XCTAssertEqual(count, incCount) // testSample(): XCTAssertEqual failed: ("0") is not equal to ("3")
        }
        
        ViewHosting.host(view: sut)
        wait(for: [exp], timeout: 2.0)
    }
}

atsu0127 avatar Sep 19 '22 03:09 atsu0127

@atsu0127 you just have to wait until it's set.

final class TestAppTests: XCTestCase {

    @MainActor
    func testSample() throws {
        let startCount = "0"
        let incCount = "3"
        var sut = ContentView()

        let e = expectation(description: "for Task")
        let exp = sut.on(\.didAppear) { view in
            var count = try view.find(viewWithTag: "count").text().string()
            XCTAssertEqual(count, startCount)

            Task {
                try view.find(viewWithTag: "updateButton").button().tap()
                try await Task.sleep(until: .now + .seconds(1), clock: .continuous)

                count = try view.find(viewWithTag: "count").text().string()
                XCTAssertEqual(count, incCount) // 🆗
                e.fulfill()
            }
        }

        ViewHosting.host(view: sut)
        wait(for: [e, exp], timeout: 2.0)
    }
}

nh7a avatar Oct 20 '23 21:10 nh7a

@nh7a Waiting for a fixed amount of time for async to finish is not optimal.

@atsu0127 I suggest you to try my approach where I track each body evaluation by index, and it allows me to write async/await tests: https://github.com/sisoje/testable-view-swiftui

I use a different production code setup and view hosting though.

sisoje avatar Feb 02 '24 17:02 sisoje