athankefalas / Stitcher

A dependency management and injection library for Swift projects
MIT License
4 stars 1 forks source link

Support mocking or replacing dependencies #6

Open ngunv13 opened 2 months ago

ngunv13 commented 2 months ago

Hello! After trying out a lot of solutions and libraries, I decided to use this great library for dependency injection in my project. I wonder how I can replace a dependency with another (mock) one in testing or in preview. I think I'll need to create some variables, apply conditional injection and invalidate the container in each test. This may affect performance and introduce some "redundant" code, but right now I cannot think of a better solution.

athankefalas commented 2 weeks ago

Hello!!! I haven't added any View level or Preview support at this time, but it is a great idea and thanks for the feedback. I think that instead of what you are proposing with conditionally adding dependencies to your app container, it might be better if try to override the active DependencyContainer entirely only in the preview context.

Until a proper upcoming release that addresses this issue and allows for interleaving of preview / non-preview dependency container, you may use the following helper functions:


// MARK: Example Service and View

protocol SomeService {

    func sayHi()
}

class SomeServiceImpl: SomeService {

    func sayHi() {
        print("Hi!")
    }
}

class SomeServiceMockImpl: SomeService {

    func sayHi() {
        print("Hi from Mock!")
    }
}

struct FirstView: View {

    class ViewModel: ObservableObject {
        @Injected
        var service: SomeService?

        init() {}
    }

    @StateObject
    var viewModel = ViewModel()

    var body: some View {
        Button("Say Hi") {
            viewModel.service?.sayHi()
        }
    }
}

// MARK: Preview

#Preview {
    withPreviewDependencies {
        Dependency(conformingTo: SomeService.self) {
            SomeServiceMockImpl()
        }
    } content: {
        FirstView()
    }
}

// MARK: Preview Dependencies Helpers

struct _WithPreviewDependenciesStaged<Content: View>: View {

    class DependencyContainerController: ObservableObject {
        let container: DependencyContainer

        init(container: DependencyContainer) {
            self.container = container
            DependencyGraph.deactivateAll()
            DependencyGraph.activate(container)
        }

        deinit {
            DependencyGraph.deactivate(container)
        }
    }

    @StateObject
    private var dependencyContainerController: DependencyContainerController

    private let content: Content

    init(
        dependencyContainer: DependencyContainer,
        content: Content
    ) {
        self.content = content
        self._dependencyContainerController = StateObject(
            wrappedValue: DependencyContainerController(
                container: dependencyContainer
            )
        )
    }

    init(
        content: Content,
        @DependencyRegistrarBuilder dependencies: @escaping () -> DependenciesRegistrar
    ) {
        self.content = content
        self._dependencyContainerController = StateObject(
            wrappedValue: DependencyContainerController(
                container: DependencyContainer(
                    dependencies: dependencies
                )
            )
        )
    }

    var body: some View {
        content
    }
}

func withPreviewDependencies<Content: View>(
    dependencies dependencyContainer: DependencyContainer,
    @ViewBuilder content: () -> Content
) -> some View {
    _WithPreviewDependenciesStaged(
        dependencyContainer: dependencyContainer,
        content: content()
    )
}

func withPreviewDependencies<Content: View>(
    @DependencyRegistrarBuilder dependencies: @escaping () -> DependenciesRegistrar,
    @ViewBuilder content: () -> Content
) -> some View {
    _WithPreviewDependenciesStaged(
        content: content(),
        dependencies: dependencies
    )
}

Important

Note that currently these helper functions will override all the active dependency containers defined in your App structure. Furthermore, you may define a DependencyContainer extension that returns the container for previews / tests.