diff --git a/Package.swift b/Package.swift index 66459c48..f0b10c2f 100644 --- a/Package.swift +++ b/Package.swift @@ -50,6 +50,7 @@ let package = Package( dependencies: [ "Dependencies", "DependenciesMacros", + .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"), ] ), diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 87e861f8..4ba71f4d 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -53,6 +53,7 @@ let package = Package( name: "DependenciesTestSupport", dependencies: [ "Dependencies", + .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"), ] ), diff --git a/Sources/DependenciesTestSupport/TestTrait.swift b/Sources/DependenciesTestSupport/TestTrait.swift index b5a3f4a7..eb06912e 100644 --- a/Sources/DependenciesTestSupport/TestTrait.swift +++ b/Sources/DependenciesTestSupport/TestTrait.swift @@ -1,4 +1,5 @@ -#if canImport(Testing) +#if canImport(Testing) && compiler(>=6) + import ConcurrencyExtras import Dependencies import Testing @@ -40,11 +41,47 @@ /// - Parameters: /// - keyPath: A key path to a dependency value. /// - value: A dependency value to override for the test. - public static func dependency( + public static func dependency( _ keyPath: WritableKeyPath & Sendable, - _ value: Value + _ value: sending Value ) -> Self { - Self { $0[keyPath: keyPath] = value } + Self { [uncheckedValue = UncheckedSendable(value)] in + $0[keyPath: keyPath] = uncheckedValue.wrappedValue + } + } + + /// A trait that overrides a test's or suite's dependency. + /// + /// Useful for overriding a dependency in a test without incurring the nesting and + /// indentation of ``withDependencies(_:operation:)-4uz6m``. + /// + /// ```swift + /// struct Client: DependencyKey { … } + /// @Test( + /// .dependency(Client.mock) + /// ) + /// func feature() { + /// // ... + /// } + /// ``` + /// + /// > Important: Due to [a Swift bug](https://github.com/swiftlang/swift/issues/76409), it is + /// > not possible to specify a closure directly inside a `@Suite` or `@Test` macro: + /// > + /// > ```swift + /// > @Suite( + /// > .dependency(Client { _ in .mock }) // 🛑 + /// > ) + /// > struct FeatureTests { /* ... */ } + /// > ``` + /// + /// - Parameters: + /// - keyPath: A key path to a dependency value. + /// - value: A dependency value to override for the test. + public static func dependency( + _ value: Value + ) -> Self where Value == Value.Value { + Self { $0[Value.self] = value } } /// A trait that overrides a test's or suite's dependencies. diff --git a/Tests/DependenciesTests/SwiftTestingTests.swift b/Tests/DependenciesTests/SwiftTestingTests.swift index f4f2b8d8..ef45b7c3 100644 --- a/Tests/DependenciesTests/SwiftTestingTests.swift +++ b/Tests/DependenciesTests/SwiftTestingTests.swift @@ -1,3 +1,5 @@ +import ConcurrencyExtras + #if canImport(Testing) import Dependencies import DependenciesTestSupport @@ -78,6 +80,18 @@ } } } + + private static let mockClient = Client { 42 } + @Test(.dependency(mockClient)) + func dependencyKeyTypeTrait() { + @Dependency(Client.self) var client + #expect(client.increment() == 42) + } + + @Test(.dependency(\.classClient, ClassClient())) + func dependencyKeyNonSendableValue() { + // NB: This test is to prove this trait compiles with a non-sendable type. + } } private struct Client: TestDependencyKey { @@ -92,4 +106,19 @@ } } } + + class ClassClient { + var count = 0 + } + extension DependencyValues { + var classClient: ClassClient { + get { self[ClassClientKey.self].wrappedValue } + set { self[ClassClientKey.self] = UncheckedSendable(newValue) } + } + } + enum ClassClientKey: TestDependencyKey { + static var testValue: UncheckedSendable { + UncheckedSendable(ClassClient()) + } + } #endif