Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve support for Previews and Unit Testing #95

Merged
merged 7 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ let package = Package(
.library(name: "XCTSpezi", targets: ["XCTSpezi"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation", .upToNextMinor(from: "0.1.0")),
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", .upToNextMinor(from: "0.2.5"))
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation", from: "1.0.0"),
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.0.0")
],
targets: [
.target(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import SwiftUI

extension AnySpezi {
/// A collection of ``Spezi/Spezi`` `LifecycleHandler`s.
private var lifecycleHandler: [LifecycleHandler] {
var lifecycleHandler: [LifecycleHandler] {
storage.collect(allOf: LifecycleHandler.self)
}

Expand Down
9 changes: 4 additions & 5 deletions Sources/Spezi/Configuration/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,16 @@
/// Defines the ``Standard`` and ``Module``s that are used in a Spezi project.
///
/// Ensure that your standard conforms to all protocols enforced by the ``Module``s. If your ``Module``s require protocol conformances
/// you must add them to your custom type conforming to ``Standard`` and passed to the initializer or extend a prebuild standard.
/// you must add them to your custom type conforming to ``Standard`` and passed to the initializer or extend a prebuilt standard.
///
/// Use ``Configuration/init(_:)`` to use default empty standard instance only conforming to ``Standard`` if you do not use any ``Module`` requiring custom protocol conformances.
///
///
/// The following example demonstrates the usage of an `ExampleStandard` standard and reusable Spezi modules, including the `HealthKit` and `QuestionnaireDataSource` modules:
/// ```swift
/// import Spezi
/// import HealthKit
/// import HealthKitDataSource
/// import Questionnaires
/// import SpeziHealthKit
/// import SpeziOnboarding
/// import SwiftUI
///
///
Expand All @@ -35,7 +34,7 @@
/// )
/// }
/// }
/// QuestionnaireDataSource()
/// OnboardingDataSource()
/// }
/// }
/// }
Expand Down
10 changes: 8 additions & 2 deletions Sources/Spezi/Spezi.docc/Spezi.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ To simplify the creation of modules, a common set of functionalities typically u
### Configuration

- <doc:Initial-Setup>
- ``SwiftUI/View/spezi(_:)``
- ``SpeziAppDelegate``
- ``Configuration``
- ``SwiftUI/View/spezi(_:)-3bn89``

### Essential Concepts

Expand All @@ -104,4 +104,10 @@ To simplify the creation of modules, a common set of functionalities typically u
- ``Spezi/Spezi``
- ``Standard``
- ``Module``


### Previews

- ``SwiftUI/View/previewWith(standard:simulateLifecycle:_:)``
- ``SwiftUI/View/previewWith(simulateLifecycle:_:)``
- ``Foundation/ProcessInfo/isPreviewSimulator``
- ``LifecycleSimulationOptions``
79 changes: 79 additions & 0 deletions Sources/Spezi/Spezi/Spezi+Preview.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Foundation
import SwiftUI
import XCTRuntimeAssertions


/// Options to simulate behavior for a ``LifecycleHandler`` in cases where there is no app delegate like in Preview setups.
public enum LifecycleSimulationOptions {
/// Simulation is disabled.
case disabled
/// The ``LifecycleHandler/willFinishLaunchingWithOptions(_:launchOptions:)-8jatp`` method will be called for all
/// configured ``Module``s that conform to ``LifecycleHandler``.
case launchWithOptions(_ launchOptions: [UIApplication.LaunchOptionsKey: Any])

static let launchWithOptions: LifecycleSimulationOptions = .launchWithOptions([:])
Supereg marked this conversation as resolved.
Show resolved Hide resolved
}


extension View {
/// Configure Spezi for your previews using a Standard and a collection of Modules.
///
/// This modifier can be used to configure Spezi with a Standard a collection of Modules without declaring a ``SpeziAppDelegate``.
///
/// - Important: This modifier is only recommended for Previews. As it doesn't configure a ``SpeziAppDelegate`` lifecycle handling
/// functionality, using ``LifecycleHandler``, of modules is not fully supported. You may use the `simulateLifecycle`
/// parameter to simulate a call to ``LifecycleHandler/willFinishLaunchingWithOptions(_:launchOptions:)-8jatp``.
///
/// - Parameters:
/// - standard: The global ``Standard`` used throughout the app to manage global data flow.
/// - simulateLifecycle: Options to simulate behavior for ``LifecycleHandler``s. Disabled by default.
/// - modules: The ``Module``s used in the Spezi project.
/// - Returns: The configured view using the Spezi framework.
public func previewWith<S: Standard>(
standard: S,
simulateLifecycle: LifecycleSimulationOptions = .disabled,
@ModuleBuilder _ modules: () -> ModuleCollection
) -> some View {
precondition(
ProcessInfo.processInfo.isPreviewSimulator,
"The Spezi previewWith(standard:_:) modifier can only used within Xcode preview processes."
)

let spezi = Spezi(standard: standard, modules: modules().elements)
let lifecycleHandlers = spezi.lifecycleHandler

return modifier(SpeziViewModifier(spezi))
.task {
if case let .launchWithOptions(options) = simulateLifecycle {
await lifecycleHandlers.willFinishLaunchingWithOptions(UIApplication.shared, launchOptions: options)
}
}

Check warning on line 58 in Sources/Spezi/Spezi/Spezi+Preview.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Spezi/Spezi/Spezi+Preview.swift#L55-L58

Added lines #L55 - L58 were not covered by tests
}

/// Configure Spezi for your previews using a collection of Modules.
///
/// This modifier can be used to configure Spezi with a collection of Modules without declaring a ``SpeziAppDelegate``.
///
/// - Important: This modifier is only recommended for Previews. As it doesn't configure a ``SpeziAppDelegate`` lifecycle handling
/// functionality, using ``LifecycleHandler``, of modules is not fully supported. You may use the `simulateLifecycle`
/// parameter to simulate a call to ``LifecycleHandler/willFinishLaunchingWithOptions(_:launchOptions:)-8jatp``.
///
/// - Parameters:
/// - simulateLifecycle: Options to simulate behavior for ``LifecycleHandler``s. Disabled by default.
/// - modules: The ``Module``s used in the Spezi project.
/// - Returns: The configured view using the Spezi framework.
public func previewWith(
simulateLifecycle: LifecycleSimulationOptions = .disabled,
@ModuleBuilder _ modules: () -> ModuleCollection
) -> some View {
previewWith(standard: DefaultStandard(), simulateLifecycle: simulateLifecycle, modules)
}
}
7 changes: 3 additions & 4 deletions Sources/Spezi/Spezi/View+Spezi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
// SPDX-License-Identifier: MIT
//

import Dispatch
import Foundation
import SwiftUI

Expand All @@ -27,9 +26,9 @@ struct SpeziViewModifier: ViewModifier {


extension View {
/// Use the `spezi()` `View` modifier to configure Spezi for your application.
/// - Parameter delegate: The `SpeziAppDelegate` used in the SwiftUI `App` instance.
/// - Returns: A SwiftUI view configured using the Spezi framework
/// Configure Spezi for your application using a delegate.
/// - Parameter delegate: The ``SpeziAppDelegate`` used in the SwiftUI App instance.
/// - Returns: The configured view using the Spezi framework.
public func spezi(_ delegate: SpeziAppDelegate) -> some View {
modifier(SpeziViewModifier(delegate.spezi))
}
Expand Down
20 changes: 20 additions & 0 deletions Sources/Spezi/Utilities/ProcessInfo+PreviewSimulator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Foundation


extension ProcessInfo {
static let xcodeRunningForPreviewKey = "XCODE_RUNNING_FOR_PREVIEWS"


/// Check if the current process is running in a simulator inside a Xcode preview.
public var isPreviewSimulator: Bool {
environment[Self.xcodeRunningForPreviewKey] == "1"
}
}
45 changes: 45 additions & 0 deletions Sources/XCTSpezi/DependencyResolution.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

@testable import Spezi
@_implementationOnly import SwiftUI


/// Configure and resolve the dependency tree for a collection of [`Module`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module)s.
///
/// This method can be used in unit test to resolve dependencies and properly initialize a set of Spezi `Module`s.
///
/// - Parameters:
/// - standard: The Spezi [`Standard`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/standard) to initialize.
/// - simulateLifecycle: Options to simulate behavior for [`LifecycleHandler`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/lifecyclehandler)s.
/// - modules: The collection of Modules that are configured.
public func withDependencyResolution<S: Standard>(
standard: S,
simulateLifecycle: LifecycleSimulationOptions = .disabled,
@ModuleBuilder _ modules: () -> ModuleCollection
) {
let spezi = Spezi(standard: standard, modules: modules().elements)

if case let .launchWithOptions(options) = simulateLifecycle {
spezi.lifecycleHandler.willFinishLaunchingWithOptions(UIApplication.shared, launchOptions: options)

Check warning on line 29 in Sources/XCTSpezi/DependencyResolution.swift

View check run for this annotation

Codecov / codecov/patch

Sources/XCTSpezi/DependencyResolution.swift#L29

Added line #L29 was not covered by tests
}
}

/// Configure and resolve the dependency tree for a collection of [`Module`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module)s.
///
/// This method can be used in unit test to resolve dependencies and properly initialize a set of Spezi `Module`s.
///
/// - Parameters:
/// - simulateLifecycle: Options to simulate behavior for [`LifecycleHandler`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/lifecyclehandler)s.
/// - modules: The collection of Modules that are configured.
public func withDependencyResolution(
simulateLifecycle: LifecycleSimulationOptions = .disabled,
@ModuleBuilder _ modules: () -> ModuleCollection
) {
withDependencyResolution(standard: DefaultStandard(), simulateLifecycle: simulateLifecycle, modules)
}
9 changes: 0 additions & 9 deletions Sources/XCTSpezi/TestAppStandard.swift

This file was deleted.

46 changes: 46 additions & 0 deletions Sources/XCTSpezi/XCTSpezi.docc/XCTSpezi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# ``XCTSpezi``

<!--

This source file is part of the Stanford Spezi open-source project

SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)

SPDX-License-Identifier: MIT

-->

Test functionality for the Spezi framework.

## Overview

This package provides several testing extensions for the Spezi framework.


### Testing Modules

Unit test are particularly useful to test the behavior of a Spezi [`Module`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module)
without building a complete SwiftUI App and testing functionality via UI Tests.
However, it might be required to resolve and configure dependencies of a `Module` before it is usable.
To do so, you can use ``withDependencyResolution(standard:simulateLifecycle:_:)`` or ``withDependencyResolution(simulateLifecycle:_:)``.
Below is a short code example that demonstrates this functionality.

```swift
import XCTSpezi

let module = ModuleUnderTest()

// resolves all dependencies and configures your module ...
withDependencyResolution {
module
}

// unit test your module ...
```

## Topics

### Modules

- ``withDependencyResolution(standard:simulateLifecycle:_:)``
- ``withDependencyResolution(simulateLifecycle:_:)``
62 changes: 62 additions & 0 deletions Tests/SpeziTests/ModuleTests/ModuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@
import SwiftUI
import XCTest
import XCTRuntimeAssertions
import XCTSpezi


private final class DependingTestModule: Module {
let expectation: XCTestExpectation
@Dependency var module = TestModule()


init(expectation: XCTestExpectation = XCTestExpectation(), dependencyExpectation: XCTestExpectation = XCTestExpectation()) {
self.expectation = expectation
self._module = Dependency(wrappedValue: TestModule(expectation: dependencyExpectation))
}


func configure() {
self.expectation.fulfill()
}
}


final class ModuleTests: XCTestCase {
Expand All @@ -23,4 +41,48 @@ final class ModuleTests: XCTestCase {
)
wait(for: [expectation])
}

func testPreviewModifier() throws {
let expectation = XCTestExpectation(description: "Preview Module")
expectation.assertForOverFulfill = true

// manually patch environment variable for running within Xcode preview window
setenv(ProcessInfo.xcodeRunningForPreviewKey, "1", 1)

_ = try XCTUnwrap(
Text("Spezi")
.previewWith {
TestModule(expectation: expectation)
}
)
wait(for: [expectation])

unsetenv(ProcessInfo.xcodeRunningForPreviewKey)
}

func testPreviewModifierOnlyWithinPreview() throws {
try XCTRuntimePrecondition {
_ = Text("Spezi")
.previewWith {
TestModule()
}
}
}

func testModuleCreation() {
let expectation = XCTestExpectation(description: "DependingTestModule")
expectation.assertForOverFulfill = true
let dependencyExpectation = XCTestExpectation(description: "TestModule")
dependencyExpectation.assertForOverFulfill = true

let module = DependingTestModule(expectation: expectation, dependencyExpectation: dependencyExpectation)

withDependencyResolution {
module
}

wait(for: [expectation, dependencyExpectation])

_ = module.module
}
}