Skip to content

Commit

Permalink
docs updates
Browse files Browse the repository at this point in the history
  • Loading branch information
mbrandonw committed Sep 11, 2024
1 parent 525a74d commit 0c06a1a
Show file tree
Hide file tree
Showing 15 changed files with 188 additions and 237 deletions.
6 changes: 3 additions & 3 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "583c00d70f39319a7eca67f614e30ccaab233ad9a104a40007e982cf4584d4d6",
"originHash" : "394d112861864bba0ea98864997e8099aeb9a5cc4c184329ffc078cf5fe4f1c0",
"pins" : [
{
"identity" : "combine-schedulers",
Expand Down Expand Up @@ -78,8 +78,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"branch" : "test-case-parameterization",
"revision" : "be48dda989581f65f82e09041b11e12da837c49d"
"revision" : "3fcc3f21695ad5bb889a024b1b046d61bebb1ef3",
"version" : "1.3.0"
}
}
],
Expand Down
44 changes: 20 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,27 +116,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 `.dependency` test trait
to override any dependencies for the scope of one single test. It's as easy as 1-2-3:

```swift
func testAdd() async throws {
let model = withDependencies {
@Test(
// 1️⃣ Override any dependencies that your feature uses.
$0.clock = ImmediateClock()
$0.date.now = Date(timeIntervalSinceReferenceDate: 1234567890)
$0.uuid = .incrementing
} operation: {
// 2️⃣ Construct the feature's model
FeatureModel()
}
.dependency(\.clock, .immediate),
.dependency(\.date.now, Date(timeIntervalSinceReferenceDate: 1234567890)),
.dependency(\.uuid, .incrementing)
)
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()
XCTAssertEqual(
model.items,
[
#expect(
model.items == [
Item(
id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!,
name: "",
Expand All @@ -158,19 +156,17 @@ But, controllable dependencies aren't only useful for tests. They can also be us
previews. Suppose the feature above makes use of a clock to sleep for an amount of time before
something happens in the view. If you don't want to literally wait for time to pass in order to see
how the view changes, you can override the clock dependency to be an "immediate" clock using the
[`withDependencies`][withdependencies-docs] helper:
`.dependencies` preview trait:

```swift
struct Feature_Previews: PreviewProvider {
static var previews: some View {
FeatureView(
model: withDependencies {
$0.clock = ImmediateClock()
} operation: {
FeatureModel()
}
)
#Preview(
traits: .dependencies {
$0.continuousClock = .immediate
}
) {
// All access of '@Dependency(\.continuousClock)' in this preview will
// use an immediate clock.
FeatureView(model: FeatureModel())
}
```

Expand Down
13 changes: 6 additions & 7 deletions Sources/Dependencies/DependencyKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,19 +162,18 @@ extension DependencyKey {
/// ``TestDependencyKey``.
public static var previewValue: Value { Self.liveValue }

/// A default implementation that provides the ``previewValue`` to XCTest runs (or ``liveValue``,
/// A default implementation that provides the ``previewValue`` to test runs (or ``liveValue``,
/// if no preview value is implemented), but will trigger a test failure when accessed.
///
/// To prevent test failures, explicitly override the dependency in any tests in which it is
/// accessed:
///
/// ```swift
/// func testFeatureThatUsesMyDependency() {
/// withDependencies {
/// $0.myDependency = .mock // Override dependency
/// } operation: {
/// // Test feature with dependency overridden
/// }
/// @Test(
/// .dependency(\.myDependency, .mock) // Override dependency
/// )
/// func featureThatUsesMyDependency() {
/// // Test feature with dependency overridden
/// }
/// ```
///
Expand Down
18 changes: 9 additions & 9 deletions Sources/Dependencies/DependencyValues/MainQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@
/// And you could test this model by overriding its main queue with a test scheduler:
///
/// ```
/// func testFeature() {
/// let mainQueue = DispatchQueue.test
/// let model = withDependencies {
/// $0.mainQueue = mainQueue.eraseToAnyScheduler()
/// } operation: {
/// TimerModel()
/// }
/// let mainQueue = DispatchQueue.test
///
/// @Test(
/// .dependency(\.mainQueue, mainQueue.eraseToAnyScheduler())
/// )
/// func feature() {
/// let model = TimerModel()
///
/// Task { await model.onAppear() }
///
/// mainQueue.advance(by: .seconds(1))
/// await mainQueue.advance(by: .seconds(1))
/// XCTAssertEqual(model.elapsed, 1)
///
/// mainQueue.advance(by: .seconds(4))
/// await mainQueue.advance(by: .seconds(4))
/// XCTAssertEqual(model.elapsed, 5)
/// }
/// ```
Expand Down
18 changes: 9 additions & 9 deletions Sources/Dependencies/DependencyValues/MainRunLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@
/// And you could test this model by overriding its main run loop with a test scheduler:
///
/// ```
/// func testFeature() {
/// let mainRunLoop = RunLoop.test
/// let model = withDependencies {
/// $0.mainRunLoop = mainRunLoop
/// } operation: {
/// TimerModel()
/// }
/// let mainRunLoop = RunLoop.test
///
/// @Test(
/// .dependency(\.mainRunLoop, mainRunLoop.eraseToAnyScheduler())
/// )
/// func feature() {
/// let model = TimerModel()
///
/// Task { await model.onAppear() }
///
/// mainRunLoop.advance(by: .seconds(1))
/// await mainRunLoop.advance(by: .seconds(1))
/// XCTAssertEqual(model.elapsed, 1)
///
/// mainRunLoop.advance(by: .seconds(4))
/// await mainRunLoop.advance(by: .seconds(4))
/// XCTAssertEqual(model.elapsed, 5)
/// }
/// ```
Expand Down
21 changes: 12 additions & 9 deletions Sources/Dependencies/DependencyValues/UUID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,23 @@ extension DependencyValues {
/// ``UUIDGenerator/incrementing`` generator as a dependency:
///
/// ```swift
/// func testFeature() {
/// let model = withDependencies {
/// $0.uuid = .incrementing
/// } operation: {
/// TodosModel()
/// }
/// @Test(
/// .dependency(\.uuid, .incrementing)
/// )
/// func feature() {
/// let model = TodosModel()
///
/// model.addButtonTapped()
/// XCTAssertEqual(
/// model.todos,
/// [Todo(id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!)]
/// #expect(
/// model.todos == [
/// Todo(id: UUID(0))
/// ]
/// )
/// }
/// ```
///
/// > Note: This test uses the special ``Foundation/UUID/init(_:)`` UUID initializer that comes
/// with this library.
public var uuid: UUIDGenerator {
get { self[UUIDGeneratorKey.self] }
set { self[UUIDGeneratorKey.self] = newValue }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,11 @@ extension DependencyValues {
/// a game's model by supplying a seeded random number generator as a dependency:
///
/// ```swift
/// func testRoll() {
/// let model = withDependencies {
/// $0.withRandomNumberGenerator = WithRandomNumberGenerator(LCRNG(seed: 0))
/// } operation: {
/// GameModel()
/// }
/// @Test(
/// .dependency(\.withRandomNumberGenerator, WithRandomNumberGenerator(LCRNG(seed: 0)))
/// )
/// func roll() {
/// let model = GameModel()
///
/// model.rollDice()
/// XCTAssert(model.dice == (1, 3))
Expand Down
68 changes: 8 additions & 60 deletions Sources/Dependencies/Documentation.docc/Articles/Lifetimes.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,55 +151,10 @@ class FeatureModel: ObservableObject {
```

Sometimes we may want to construct this model in a "controlled" environment, where we use a
different implementation of `apiClient`. Tests are probably the most prototypical example of this.
In tests we do not want to make a live network request since that opens up to the vagaries of the
outside world, and instead we want to provide an implementation of the `apiClient` that
synchronously and immediately return some data so that you can test how that data flows through your
features logic.
different implementation of `apiClient`.

The library comes with a helper in order to do this and it's called
``withDependencies(_:operation:)-4uz6m``. It takes two closures: the first allows you
to override any dependencies you want, and the second allows you to execute your feature's logic in
a scope where those dependency mutations are applied:

```swift
func testOnAppear() async {
await withDependencies {
$0.apiClient.fetchUser = { _ in User(id: 42, name: "Blob") }
} operation: {
let model = FeatureModel()
XCTAssertEqual(model.user, nil)
await model.onAppear()
XCTAssertEqual(model.user, User(id: 42, name: "Blob"))
}
}
```

All code executed in the `operation` trailing closure of
``withDependencies(_:operation:)-4uz6m`` will use the overridden `fetchUser`
endpoint, which makes it possible to exercise the feature's code without making a real network
request.

But, we can take this one step further. We don't need to execute the entire test in the scope of the
trailing `operation` closure. We only need to construct the model in that scope, and then as long as
all dependencies are declared in `FeatureModel` as instance variables, all interactions with the
model will use the controlled dependencies, even outside the `operation` closure:

```swift
func testOnAppear() async {
let model = withDependencies {
$0.apiClient.fetchUser = { _ in User(id: 42, name: "Blob") }
} operation: {
FeatureModel()
}

XCTAssertEqual(model.user, nil)
await model.onAppear()
XCTAssertEqual(model.user, User(id: 42, name: "Blob"))
}
```

This is one way in which `@Dependency` can propagate changes outside of its standard scope.
> Note: Tests are probably the most prototypical example of overriding dependencies to control them.
Be sure to read the dedicated article <doc:Testing> for more information on that topic.
Controlling dependencies isn't only useful in tests. It can also be used directly in your feature's
logic in order to run some child feature in a controlled environment, and can even be used in Xcode
Expand Down Expand Up @@ -259,19 +214,12 @@ feature behaves in very specific states. For example, if you wanted to see how y
when the `fetchUser` endpoint throws an error, you can update the preview like so:

```swift
struct Feature_Previews: PreviewProvider {
static var previews: some View {
FeatureView(
model: withDependencies {
$0.apiClient.fetchUser = { _ in
struct SomeError: Error {}
throw SomeError()
}
} operation: {
FeatureModel()
}
)
#Preview(
traits: .dependencies {
$0.apiClient.fetchUser = { _ in throw SomeError() }
}
) {
FeatureView(model: FeatureModel())
}
```

Expand Down
34 changes: 17 additions & 17 deletions Sources/Dependencies/Documentation.docc/Articles/LivePreviewTest.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ Using live dependencies in tests are so problematic that the library will cause
if you ever interact with a live dependency while tests are running:

```swift
func testFeature() async throws {
@Test
func feature() async throws {
let model = FeatureModel()

model.addButtonTapped()
Expand All @@ -53,19 +54,17 @@ func testFeature() async throws {
}
```


If you truly want to use
live dependencies in tests you have to make it explicit by overriding the dependency using
``withDependencies(_:operation:)-3vrqy`` and setting the live value:
live dependencies in tests you have to make it explicit by overriding the dependency the
`.dependency` testing trait and setting the live value:

```swift
func testFeature() async throws {
let model = withDependencies {
// ⚠️ Explicitly say you want to use a live dependency.
$0.apiClient = .liveValue
} operation: {
FeatureModel()
}
@Test(
// ⚠️ Explicitly say you want to use a live dependency.
.dependency(\.apiClient, .liveValue)
)
func feature() async throws {
let model = FeatureModel()

// ...
}
Expand Down Expand Up @@ -267,12 +266,13 @@ understand the risks of using a live dependency in tests. To confirm that you tr
live dependency you can override the dependency with `.liveValue`:

```swift
func testFeature() {
let model = withDependencies {
$0.apiClient = .liveValue // ⬅️
} operation: {
FeatureModel()
}
@Test(
// ⚠️ Explicitly say you want to use a live dependency.
.dependency(\.apiClient, .liveValue)
)
func feature() async throws {
let model = FeatureModel()

// ...
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,15 @@ a user when the view appears, a test for this functionality could be written by
`apiClient` to return some mock data:

```swift
func testOnAppear() async {
let model = withDependencies {
$0.apiClient.fetchUser = { _ in User(id: 42, name: "Blob") }
} operation: {
FeatureModel()
}
@Test(
.dependency(\.apiClient.fetchUser, { _ in User(id: 42, name: "Blob") })
)
func onAppear() async {
let model = FeatureModel()

XCTAssertEqual(model.user, nil)
#expect(model.user == nil)
await model.onAppear()
XCTAssertEqual(model.user, User(id: 42, name: "Blob"))
#expect(model.user == User(id: 42, name: "Blob"))
}
```

Expand Down
Loading

0 comments on commit 0c06a1a

Please sign in to comment.