StanfordSpezi / Spezi

Open-source framework for rapid development of modern, interoperable digital health applications.
https://swiftpackageindex.com/StanfordSpezi/Spezi/documentation
MIT License
130 stars 10 forks source link

Extend XCTSpezi to Support the Individual Creation of Modules and Components #64

Closed PSchmiedmayer closed 6 months ago

PSchmiedmayer commented 1 year ago

Problem

Similar to #63, the creation of modules and components in the setting of a unit test is essential to investigate behavior of components in a unit test.

The current code in unit test used in the Spezi ecosystem looks something like the following example from the Spezi Scheduler module:

let scheduler = Scheduler<SchedulerTestsStandard, String>(tasks: [initialTasks])
let localStorageDependency = Mirror(reflecting: scheduler).children
    .compactMap {
        $0.value as? _DependencyPropertyWrapper<LocalStorage<SchedulerTestsStandard>, SchedulerTestsStandard>
    }
    .first
localStorageDependency?.inject(dependency: LocalStorage())
scheduler.configure()

Manually iterating over the properties using a Mirror might not be the desirable behaviour and can lead to errors and undefined behaviour.

Solution

The XCTSpezi Module should provide an easy mechanism to create components/modules in unit tests and needs to provide a mechanism to define the injected components or create components if they are default initializable.

The exact design of the API is up to debate. Feel free to add a comment in this issue about design ideas and concepts around the creation of the API and how you would like to implement the feature.

Additional context

No response

Code of Conduct

Supereg commented 6 months ago

With recent changes in the Spezi framework, I wanted to provide some additional context to this issue.

The latest release of SpeziScheduler uses the following, greatly simplified setup to support the individual creation of Modules:

@testable import Spezi
@testable import SpeziScheduler

let scheduler = Scheduler<String>(tasks: [initialTasks])

// use the internal initializer to resolve dependencies via the `DependencyManager`
 _ = Spezi(standard: DefaultStandard(), modules: [scheduler])

// allow for async configuration of modules
try? await _Concurrency.Task.sleep(for: .seconds(0.1))

// use the `scheduler` ...

By making the Spezi import @testable, we gain access to the internal initializer of the Spezi type. This is great as it uses the exact same mechanisms as the spezi(_:) to resolve the dependency tree and doesn't rely on any unsafe usage of Mirror.

What is up to discussion is how we provide access to this API surface. Requiring a @testable import might not be the best option and the current initializer of the Spezi type is currently not really meant for external usage.

Further, some modules might do some asynchronous initialization after configuration. Certain users might want to wait for completion for these operations.

EDIT: Of course, as we do not attach to the SwiftUI view hierarchy, this doesn't set up an AppDelegate and therefore a LifecycleHandler will never be called.

Supereg commented 6 months ago

Feedback as discussed in the meeting:

Supereg commented 6 months ago

Completed as of #95