Search code examples
iosswiftuixctest

Mock a SwiftUI view from another module


I'm trying to test a SwiftUI view that has a subview from another module in its body:

import SwiftUI
import Abond

struct ProfileView: PresentableView, LoadedView {
    @State var isLoading = true

    public var body: some View {
        Load(self) {
            AbondProfile(onSuccess: self.onSubmitSuccess)
        }
    }

    func load() -> Binding<Bool>  {
        ProfileApi.getProfileAccessToken() { result in
            switch result {
            case .success(let response):
                Abond.accessToken = response.accessToken
            case .failure(let error):
                print("error getting token")
            }
            isLoading = false
        }
        return $isLoading
    }

    func onSubmitSuccess() {
        print("success")
    }
}

My question is: if I want to test the lifecycle of ProfileView without the actual AbondProfile view being built, is there a way to mock that? If it were a normal method I would inject a dependency object, but I don't know how to translate that to a struct initializer.

Abond is a Swift Package, so I can't modify AbondProfile. And I'd prefer to be able to test this with as little change to my view code as possible. I'm using XCTest.


Solution

  • As David Wheeler said, “Any problem in computer science can be solved with another level of indirection.”

    In this case, one solution is to refer to AbondProfile indirectly, through a generic type parameter. We add a type parameter to ProfileView to replace the direct use of AbondProfile:

    struct ProfileView<Content: View>: PresentableView, LoadedView {
        @State var isLoading = true
        @ViewBuilder var content: (_ onSuccess: @escaping () -> Void) -> Content
    
        public var body: some View {
            Load(self) {
                content(onSubmitSuccess)
            }
        }
    
        blah blah blah
    }
    

    We don't have to change current uses of ProfileView if we provide a default initializer that uses AbondProfile:

    extension ProfileView {
        init() where Content == AbondProfile {
            self.init { AbondProfile(onSuccess: $0) }
        }
    }
    
    struct ProductionView: View {
        var body: some View {
            ProfileView() // This uses AbondProfile.
        }
    }
    

    And in a test, we can provide a mock view:

    struct TestView: View {
        var body: some View {
            ProfileView { onSuccess in
                Text("a travesty of a mockery of a sham of a mockery of a travesty of two mockeries of a sham")
            }
        }
    }