Reputation: 9828
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
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
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