mckeed
mckeed

Reputation: 9828

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.

Upvotes: 1

Views: 473

Answers (2)

mckeed
mckeed

Reputation: 9828

I accepted the other answer because it's a more proper solution, but I found that it actually works to just redefine the struct in your test file:

import XCTest
import Abond
import SwiftUI

// Mock for Abond.AbondProfile
public struct AbondProfile: View {
    static var viewDidAppearCallback: (() -> Void)?
    static var submit: (() -> Void)?

    public init(onSuccess: (() -> Void)? = nil) {
        AbondProfile.submit = onSuccess
    }

    public var body: some View {
        Text(Abond.accessToken)
            .onAppear {
                AbondProfile.viewDidAppearCallback?()
            }
    }
}

class ProfileViewTests: BaseViewControllerTests {
    private var viewController: UIViewController?

    func testSucesss() {
        let viewDidAppearExpectation = XCTestExpectation(description: "View did appear")
        AbondProfile.viewDidAppearCallback = { viewDidAppearExpectation.fulfill() }
        MockApi.mockRequest(ProfileApi.getProfileAccessToken, response: ProfileAccessToken(accessToken:"accessToken_123"))

        initialize(viewController: UIHostingController(rootView: ProfileView()))

        wait(for: [viewDidAppearExpectation], timeout: 10)
        XCTAssertEqual(Abond.accessToken, "accessToken_123")

        AbondProfile.submit!()
        // etc.
    }
}

I'm aware the static variables make the test brittle – but other than that, I'd be interested to hear if there are any other reasons not to do it this way.

Upvotes: 0

rob mayoff
rob mayoff

Reputation: 385660

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")
        }
    }
}

Upvotes: 3

Related Questions