Skip to content
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
27 changes: 0 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,30 +74,3 @@ jobs:
swift-configuration: ${{ matrix.config }}
run-tests: false
build-tests: false

# windows:
# name: Windows
# runs-on: windows-latest
# steps:
# - uses: compnerd/gha-setup-swift@main
# with:
# branch: swift-5.10-release
# tag: 5.10-RELEASE
#
# - uses: actions/checkout@v4
# - name: Run tests
# run: swift test
# - name: Run tests (release)
# run: swift test -c release

check-macro-compatibility:
name: Check Macro Compatibility
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run Swift Macro Compatibility Check
uses: Matejkob/swift-macro-compatibility-check@v1
with:
run-tests: false
major-versions-only: true
2 changes: 1 addition & 1 deletion Package@swift-6.0.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-clocks", from: "1.0.4"),
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.4.0"),
.package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"603.0.0"),
.package(url: "https://github.com/swiftlang/swift-syntax", "600.0.0"..<"603.0.0"),
],
targets: [
.target(
Expand Down
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,21 +124,25 @@ That is all it takes to start using controllable dependencies in your features.
bit of upfront work done you can start to take advantage of the library's powers.

For example, you can easily control these dependencies in tests. If you want to test the logic
inside the `addButtonTapped` method, you can use the [`withDependencies`][withdependencies-docs]
function to override any dependencies for the scope of one single test. It's as easy as 1-2-3:
inside the `addButtonTapped` method, you can use the `.dependencies` test trait
to override any dependencies for the scope of one single test. It's as easy as 1-2-3:

```swift
@Test
func add() async throws {
let model = withDependencies {
// 1️⃣ Override any dependencies that your feature uses.
import Dependencies
import DependenciesTestSupport
import Testing

@Test(
// 1️⃣ Override any dependencies that your feature uses.
.dependencies {
$0.clock = .immediate
$0.date.now = Date(timeIntervalSinceReferenceDate: 1234567890)
$0.uuid = .incrementing
} operation: {
// 2️⃣ Construct the feature's model
FeatureModel()
}
)
func add() async throws {
// 2️⃣ Construct the feature's model
let model = FeatureModel()
// 3️⃣ The model now executes in a controlled environment of dependencies,
// and so we can make assertions against its behavior.
try await model.addButtonTapped()
Expand Down
23 changes: 13 additions & 10 deletions Sources/Dependencies/Documentation.docc/Articles/QuickStart.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,25 @@ That is all it takes to start using controllable dependencies in your features.
bit of upfront work done you can start to take advantage of the library's powers.

For example, you can easily control these dependencies in tests. If you want to test the logic
inside the `addButtonTapped` method, you can use the ``withDependencies(_:operation:)-4uz6m``
function to override any dependencies for the scope of one single test. It's as easy as 1-2-3:
inside the `addButtonTapped` method, you can use the `.dependencies` test trait
to override any dependencies for the scope of one single test. It's as easy as 1-2-3:

```swift
@Test
func add() async throws {
let model = withDependencies {
// 1️⃣ Override any dependencies that your feature uses.
import Dependencies
import DependenciesTestSupport
import Testing

@Test(
// 1️⃣ Override any dependencies that your feature uses.
.dependencies {
$0.clock = .immediate
$0.date.now = Date(timeIntervalSinceReferenceDate: 1234567890)
$0.uuid = .incrementing
} operation: {
// 2️⃣ Construct the feature's model
FeatureModel()
}

)
func add() async throws {
// 2️⃣ Construct the feature's model
let model = FeatureModel()
// 3️⃣ The model now executes in a controlled environment of dependencies,
// and so we can make assertions against its behavior.
try await model.addButtonTapped()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,45 @@ when you want to register your own dependencies with the library so that you can
``Dependency`` property wrapper. There are a couple ways to achieve this, and the process is quite
similar to registering a value with [the environment][environment-values-docs] in SwiftUI.

## The @DependencyEntry macro

This simplest way to register a dependency is using the `@DependencyEntry` macro. Simply extend
the ``DependencyValues`` type and apply the macro on a mutable property with a default value:

```swift
import Dependencies
import DependenciesMacros

extension DependencyValues {
@DependencyEntry
var apiClient: any APIClient = MockAPIClient()
}
```

This will create a private inner type that conforms to the ``TestDependencyKey`` protocol and
provides a ``TestDependencyKey/testValue`` of `MockAPIClient`.

If it is appropriate to also define the ``DependencyKey/liveValue`` in the same module as the
``TestDependencyKey/testValue`` then you can do so by providing a `liveValue` argument:

```swift
import Dependencies
import DependenciesMacros

extension DependencyValues {
@DependencyEntry(liveValue: LiveAPIClient())
var apiClient: any APIClient = MockAPIClient()
}
```

However, if the live implementation of the dependency is only appropriate to define at the entry
point of the app, or if you need to keep the live implementation separate from the dependency
interface, you will not provide this argument. And instead you will employ the techniques in
<doc:LivePreviewTest#Separating-interface-and-implementation>.

## Manual conformance to DependencyKey

You can also conform to ``TestDependencyKey`` and ``DependencyKey`` directly.
First you create a ``DependencyKey`` protocol conformance. The minimum implementation you must
provide is a ``DependencyKey/liveValue``, which is the value used when running the app in a
simulator or on device, and so it's appropriate for it to actually make network requests to an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,19 @@ Then, all 3 dependencies can easily be overridden with deterministic versions wh
feature:

```swift
@MainActor
@Test
func todos() async {
let model = withDependencies {
import Dependencies
import DependenciesTestSupport
import Testing

@Test(
.dependencies {
$0.continuousClock = .immediate
$0.date.now = Date(timeIntervalSinceReferenceDate: 1234567890)
$0.uuid = .incrementing
} operation: {
TodosModel()
}

)
func todos() async {
let model = TodosModel()
// Invoke methods on `model` and make assertions...
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,21 @@ This will cause the message to appear immediately. No need to wait 10 seconds.
> Tip: We have a [series of episodes][clocks-collection] discussing the `Clock` protocol in depth
and showing how it can be used to control time-based asynchrony.

Further, in tests you can also override the clock dependency to use an immediate clock, also using
the ``withDependencies(_:operation:)-4uz6m`` helper:
Further, in tests you can also override the clock dependency to use an immediate clock, using
the `.dependencies` test trait:

```swift
@Test
func message() async {
let model = withDependencies {
import Dependencies
import DependenciesTestSupport
import Testing

@Test(
.dependencies {
$0.continuousClock = .immediate
} operation: {
FeatureModel()
}

)
func message() async {
let model = FeatureModel()
#expect(model.message == nil)
await model.onAppear()
#expect(model.message == "Welcome!")
Expand Down
48 changes: 48 additions & 0 deletions Sources/DependenciesMacros/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,54 @@ public macro DependencyEndpoint(method: String = "") =
public macro DependencyEndpointIgnored() =
#externalMacro(module: "DependenciesMacrosPlugin", type: "DependencyEndpointIgnoredMacro")

/// Creates a dependency values entry.
///
/// Use this macro to register a custom dependency on ``DependencyValues`` without having to declare
/// a separate ``TestDependencyKey`` or ``DependencyKey`` conformance:
///
/// ```swift
/// extension DependencyValues {
/// @DependencyEntry var apiClient: any APIClient = MockAPIClient()
/// }
/// ```
///
/// The macro will synthesize a private key type behind the scenes and generate the property's
/// `get`/`set` accessors with the following rules:
///
/// * The value provided to the `@DependencyEntry` is used as the ``TestDependencyKey/testValue``
/// in the ``TestDependencyKey`` conformance.
/// * If the `liveValue` argument is provided to `@DependencyEntry`, then the synthesized key
/// type will conform to ``DependencyKey`` and provide the specified
/// ``DependencyKey/liveValue``:
///
/// ```swift
/// extension DependencyValues {
/// @DependencyEntry(liveValue: LiveAPIClient())
/// var apiClient: any APIClient = MockAPIClient()
/// }
/// ```
///
/// If you want to separate the live implementation from the interface of your dependency, you will
/// need to leave off the `liveValue` argument and instead provide the `liveValue` in your main
/// app target, as described in <doc:LivePreviewTest:Separating-interface-and-implementation>.
///
/// - Parameters:
/// - liveValue: A live value.
/// - previewValue: A preview value.
@attached(accessor, names: named(get), named(set))
@attached(peer, names: prefixed(__Key_))
public macro DependencyEntry<LiveValue, PreviewValue>(
liveValue: LiveValue = (),
previewValue: PreviewValue = ()
) = #externalMacro(module: "DependenciesMacrosPlugin", type: "DependencyEntryMacro")

@attached(accessor, names: named(get))
public macro _DependencyEntryDefaultValue() =

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining this in DependenciesMacros for now, but we could probably flatten these modules soon.

#externalMacro(
module: "DependenciesMacrosPlugin",
type: "DependencyEntryDefaultValueMacro"
)

/// The error thrown by "unimplemented" closures produced by ``DependencyEndpoint(method:)``
public struct Unimplemented: Error {
let endpoint: String
Expand Down
Loading
Loading