Mocking view model in swiftui with combine

Viewed 879

Is there a way to mock the viewmodels of an application that uses SwiftUI and Combine? I always find articles about mocking services used by the viewmodel but never mocking a viewmodel itself.

I tried to create protocols for each viewmodel. Problem: the @Published wrapper cannot be used in protocols. It seems like there are no solutions...

Thanks for your help

2 Answers

Using a protocol type as @ObservableObject or @StateObject would not work. Inheritance might be a solution (like Jake suggested), or you could go with a generic solution.

protocol ContentViewModel: ObservableObject {
    var message: String { get set }
}

Your view model would be simple.

final class MyViewModel: ContentViewModel {
    
    @Published var message: String
    init(_ message: String = "MyViewModel") {
        self.message = message
    }
}

On the other hand, your views would be more complex using a constrained generic.

struct ContentView<Model>: View where Model: ContentViewModel {
    @ObservedObject
    var viewModel: Model
    
    var body: some View {
        VStack {
            Text(viewModel.message)
            Button("Change message") {
                viewModel.message = ""
            }
        }
    }
}

The disadvantage is that you have to define the generic concrete type when using the view --- inheritance could avoid that.

// your mock implementation for testing
final class MockViewModel: ContentViewModel {
    @Published var message: String = "Mock View Model"
}

let sut = ContentView<MockViewModel>(viewModel: MockViewModel())

If the view model is only a model, you should probably not mock that at all, instead you would mock the thing that modifies the view model. If your view model actually owns the functions and things that updates itself, then you would want to mock it directly. In which case you can use protocols to mock the view model. It would look a little like this.

protocol ViewModel {
    var title: String { get set }
}

class MyViewModel: ViewModel {
    @Published var title: String = "Page title"
}

class MockViewModel: ViewModel {
    @Published var title: String = "MockPage title"
}

However, this is probably an instance where I would prefer inheriting a class instead of adhering to a protocol. Then I would override the functionality of the class for the mock.

open class ViewModel {
    @Published var title: String

    open fun getPageTitle() {
        title = "This is the page title"
    }
}

class MockViewModel: ViewModel {
    override fun getPageTitle() {
        title = "some other page title"
    }
}

Either way would work really. The inheritance way is just less verbose in your test suite if your View Model has functionality too.

Related