QuickBirdEng / XCoordinator

🎌 Powerful navigation library for iOS based on the coordinator pattern
MIT License
2.27k stars 179 forks source link

How do you manage dependencies when testing? #90

Closed Ricardo1980 closed 5 years ago

Ricardo1980 commented 5 years ago

Hello,

There is something I don't know exactly how to do.

My view models have a list of dependencies in the initialiser, same for the enums in the route. So, I have something like:

enum BudgetRoute: Route {
    case budgetController(service: BudgetServiceProtocol, budgetDefaultOn: Bool, delegate: BudgetStatusDelegate)
    case tariffAlertController(service: TariffServiceProtocol, delegate: TariffAlertDelegate)
    case tariffController(service: TariffServiceProtocol)
    case dismiss
}

And the coordinator is:

final class BudgetCoordinator: NavigationCoordinator<BudgetRoute> {
    override func prepareTransition(for route: BudgetRoute) -> NavigationTransition {
        switch route {
        case let .budgetController(service, budgetDefaultOn, delegate):
            var vc = BudgetViewController.instantiateController()
            let viewModel = BudgetViewModel(coordinator: anyRouter,
                                            service: service,
                                            budgetDefaultOn: budgetDefaultOn,
                                            delegate: delegate)
            vc.bind(to: viewModel)
            return .push(vc)
        case let .tariffAlertController(service, delegate):
            var vc = TariffAlertController.instantiateController()
            let viewModel = TariffAlertViewModel(coordinator: anyRouter,
                                                 service: service,
                                                 delegate: delegate)
            vc.bind(to: viewModel)
            return .push(vc)
        case let .tariffController(service):
            let homeCoordinator = HomeCoordinator(initialRoute: .tariffController(service: service))
            let coordinator = AnyRouter<HomeRoute>(homeCoordinator)

            var vc = TariffController.instantiateController()
            let viewModel = TariffViewModel(coordinator: coordinator, service: service)
            vc.bind(to: viewModel)
            return .push(vc)
        case .dismiss:
            return .dismiss()
        }
    }
}

You can see how I pass my dependencies. The problem I have is that sometimes the view mode calls something like:

                    self.coordinator.trigger(.budgetController(service: BudgetService(),
                                                               budgetDefaultOn: false,
                                                               delegate: self.budgetStatusDelegate))

Which means that I cannot test that scenario properly, because I cannot pass mocks. Check "service: BudgetService()", that is not a mock, but a real service.

How should I manage that? Should I pass to my view model all dependencies? I mean, even the ones that are going to be called when opening other view models when calling trigger (XCoordinator)?

Thanks a lot for suggestions.

Ricardo1980 commented 5 years ago

Well, I found another way, I think it make sense. Basically, I delegate all dependency injection to the coordinators, then, when testing a specific view controller/view model, I pass a coordinator that is a subclass of the original coordinator but using makes. Doing that, I don't use the enum for passing dependencies, just relevant parameters (like ids or whatever). What do you think?

pauljohanneskraft commented 5 years ago

I'm not sure about what you mean exactly, but what we often do is to create services in the coordinator rather than in (often unrelated) viewModels. That way, you can simply take a look at the coordinators to change any services. If that becomes too complicated and confusing, there is also the possibility of introducing a serviceFactory or serviceLocator, which is only known to the coordinators.

Ricardo1980 commented 5 years ago

Thanks @pauljohanneskraft I was creating the services in the view models, and that was a problem. Now I moved that to the coordinators, and then I can test everything properly, I only have to inject a mock coordinator.