EP 114 · Designing Dependencies · Aug 24, 2020 ·Members

Video #114: Designing Dependencies: The Point

smart_display

Loading stream…

Video #114: Designing Dependencies: The Point

Episode: Video #114 Date: Aug 24, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep114-designing-dependencies-the-point

Episode thumbnail

Description

So, what’s the point of forgoing the protocols and designing dependencies with simple data types? It can be summed up in 3 words: testing, testing, testing. We can now easily write tests that exercise every aspect of our application, including its reliance on internet connectivity and location services.

Video

Cloudflare Stream video ID: 37a63646b730b223c8f1f1f2208ad207 Local file: video_114_designing-dependencies-the-point.mp4 *(download with --video 114)*

Transcript

0:05

We’ve now got a moderately complex application that makes non-trivial use of 3 dependencies: a weather API client, a network path monitor, and a location manager. The weather API dependency was quite simple in that it merely fires off network requests that can either return some data or fail. The path monitor client was a bit more complex in that it bundled up the idea of starting a long-living effect that emits network paths over time as the device connection changes, as well as the ability to cancel that effect. And Core Location was the most complicated by far. It allows you to ask the user for access to their location, and then request their location information, which is a pretty complex state machine that needs to be carefully considered.

1:07

So we’ve accomplished some cool things, but I think it’s about time to ask “what’s the point?” We like to do this at the end of each series of episodes on Point-Free because it gives us the opportunity to truly justify all the work we have accomplished so far, and prove that the things we are discussing have real life benefits, and that you could make use of these techniques today.

1:28

And this is an important question to ask here because we are advocating for something that isn’t super common in the Swift community. The community already has an answer for managing and controlling dependencies, and that’s protocols. You slap a protocol in front of your dependency, make a live conformance and a mock conformance of the protocol, and then you are good to go. We showed that this protocol-oriented style comes with a bit of boilerplate, but if that’s the only difference from our approach is it really worth deviating from it?

1:57

Well, we definitely say that yes, designing your dependencies in this style has tons of benefits, beyond removing some boilerplate. And to prove this we are going to write a full test suite for our feature, which would have been much more difficult to do had we controlled things in a more typical, protocol-oriented fashion. Testing our feature

2:25

We can clean up our WeatherFeatureTests case and add a single test. import XCTest @testable import WeatherFeature class WeatherFeatureTests: XCTestCase { func testBasics() { } }

2:43

In here we are going to get our feet wet by testing the most basic functionality of the feature. We want to start the feature in the most ideal state, one where we have internet connectivity and full access to location, and then we will simply assert that weather results are fetched instantly.

3:07

To do this let’s see what it takes to even instantiate a view model: let viewModel = AppViewModel( locationClient: <#LocationClient#>, pathMonitorClient: <#PathMonitorClient#>, weatherClient: <#WeatherClient#> )

3:18

We need to provide all of our dependencies: the location client, the path monitor client, and the weather client. So we can import our client modules and start filling the dependencies in. import LocationClient import PathMonitorClient import WeatherClient

3:40

Note that these dependencies only include the struct interfaces to our dependencies, which compiles super fast, unlike the live dependencies, which can take some time to build.

4:01

For the location client we already have a mock that does exactly what we want: it is pre-authorized for location access, and when you request a location from it you instantly get back a mock CLLocation . So let’s use it: let viewModel = AppViewModel( locationClient: .authorizedWhenInUse, pathMonitorClient: <#PathMonitorClient#>, weatherClient: <#WeatherClient#> )

4:18

The path monitor client is just as easy to fill in because we also have a mock of that client that simulates the situation where our device has perfect internet connectivity, which is what we want: let viewModel = AppViewModel( locationClient: .authorizedWhenInUse, pathMonitorClient: .satisfied, weatherClient: <#WeatherClient#> )

4:42

Finally we need to fill in our weather client. We could hit the ground running and again choose from our growing collection of mocks, like the .happyPath , which successfully returns some weather, but because the data returned from the weather endpoint is what we ultimately want to assert against, we should be a bit more deliberate in controlling it here, with data local to the test that we can directly assert against. The .empty , .failed , and .happyPath mocks were specifically made for our SwiftUI previews, and it would be strange if changing one to see the preview update also inadvertently broke tests, so instead we will be creating a weather client from scratch: weatherClient: WeatherClient( weather: <#(Int) -> AnyPublisher<WeatherResponse, Error>#>, searchLocations: <#(CLLocationCoordinate2D) -> AnyPublisher<[Location], Error>#> )

5:55

And while you may be asking why we didn’t also create the location client and path monitor client from scratch, there is very little extra data attached to these clients that would affect the UI of previews and tests we use them in, so we should feel safe to share them for now. But meanwhile, the weather client has some very specific data attached, like location name and temperatures that directly impact the UI.

6:34

The weather endpoint will need to return a weather response we can assert against, so let’s create a mock from scratch: let moderateWeather = WeatherResponse( consolidatedWeather: [ .init( applicableDate: Date(timeIntervalSinceReferenceDate: 0), id: 1, maxTemp: 30, minTemp: 20, theTemp: 25 ), ] )

7:08

Which we can stub in the weather endpoint of the client: weather: { _ in Just(moderateWeather).setFailureType(to: Error.self).eraseToAnyPublisher() },

7:35

Similarly, we can create a mock location: let brooklyn = Location(woeid: 1, title: "Brooklyn") And stub it in the searchLocations endpoint: searchLocations: { _ in Just([brooklyn]).setFailureType(to: Error.self).eraseToAnyPublisher() }

8:15

And with that setup work we can write our first assertions. We will assert against all of the published fields on the view model, since that is the data that feeds into the UI and controls what is rendered. The first field is currentLocation , and we expect it to hold the Brooklyn location we created: XCTAssertEqual(viewModel.currentLocation, brooklyn)

9:32

If we try to run our tests, nothing happens, and that’s because we’re building the framework target, but its associated tests are only set up for the app target. For whatever reason, framework tests are only added to the app scheme by default.

9:50

And now tests are set up to build.

10:39

But when we try…

10:40

Well that’s no good, it’s taking a very long time to build, and that’s because we seem to be building the entire application, and the application depends on the live weather client, which is a module that takes a very long time to build.

10:50

We only care about testing the framework, so why are we building the entire application? Well, when you add a new framework to a project, as we did when we extracted out our feature, Xcode makes a few decisions for you: it automatically embeds the framework in your application target, and it configures the framework’s tests to use the application as the test host.

11:05

First, we will remove the app target as a test dependency. This should speed up build times because we won’t have to build any of the live implementations of our dependencies.

11:12

And now when we try to run our tests we get a crash. Undefined symbols for architecture x86_64: LocationClient.LocationClient.init Undefined symbols for architecture x86_64: PathMonitorClient.PathMonitorClient.init Undefined symbols for architecture x86_64: WeatherClient.WeatherClient.init And this is because we need to add all of our dependencies to WeatherFeatureTests to fix.

11:34

But this still isn’t enough to build the tests quickly. We must also change the test’s host application to “None”. Otherwise we will still be dependent on building the entire application to run tests.

11:54

And now when we run the test we see it passes! This may not seem like a big deal, but recall that the initial value of this field is nil , which means in order for it to be set it, some view model logic was actually executed, and this already exercises our dependencies quite a bit.

13:11

Next we have the isConnected field, which we expect to be true: XCTAssertEqual(viewModel.isConnected, true)

13:36

That’s not all that impressive since that’s the value the field starts out at, but soon we will be able to test what happens when we lose connectivity in the view model.

13:41

And finally we have the weather results field: XCTAssertEqual(viewModel.weatherResults, moderateWeather.consolidatedWeather)

14:07

Again, this may not seem super impressive, but remember that the only way for our moderate weather results to make their way into the view model is if:

14:20

The view model checked the authorization status and saw that we were authorized We request the user’s current location and get a result back via the delegate method We request the location from the weather API using the latitude and longitude coordinates And then finally we request the weather from the weather API using the location id

14:46

So this is a pretty deep test. It is exercising the whole stack of dependencies and how they work in concert. To prove this to ourselves, let’s mock out a few of these endpoints with fatalError s just so that we can see they are definitely being called: //locationClient: .authorized, locationClient: LocationClient( authorizationStatus: { fatalError() }, requestWhenInUseAuthorization: { fatalError() }, requestLocation: { fatalError() }, delegate: Deferred { Future { _ in fatalError() }.eraseToAnyPublisher() } ),

15:55

When we run tests we see we get caught on the delegate endpoint because the view model is definitely executing that code path. So let’s reinsert the endpoint using an .authorized client and see what happens: let locationClient = LocationClient.authorized … delegate: locationClient.delegate,

16:34

Next we get caught on the authorizationStatus because one of the first things the view model does is ask for it in order to know whether it needs to request access or not. Let’s put that endpoint back in and continue: authorizationStatus: locationClient.authorizationStatus,

17:00

Now we get caught on requestLocation , which happens because once the view model sees that we are authorized for location access it makes a request for the current location. Let’s put that endpoint back and continue: requestLocation: locationClient.requestLocation

17:24

Now tests actually run and pass. So it turns out that in this test the requestWhenInUseAuthorization endpoint isn’t needed, which makes sense because we started the location client off in the authorized state.

17:42

What we have done here is incredibly powerful, and is simply not possible with protocols, or is so cumbersome that you would never actually do it. We have been able to mark every single endpoint of the location client as unimplemented, and step-by-step reintroduce proper implementations to see when and how the dependencies are used.

18:16

In fact, we might even go as far to say that this is the correct way to deal with dependencies in tests. You can strengthen your tests by putting fatalError s in all endpoints that you don’t expect to be used in the test. This requires you to further prove that you know exactly which dependencies are used and how.

18:38

Let’s make one more tweak to our dependencies to even further prove that our test really is going through the full stack of dependencies to do its job. We are going to swap out the delegate publisher with one that never emits, which means a location will never be delivered when we request it: delegate: Empty(completeImmediately: false).eraseToAnyPublisher(),

19:09

Now when we run tests we get two failures: XCTAssertEqual failed: (“nil”) is not equal to (“Optional(WeatherClient.Location(title: “Brooklyn”, woeid: 1))”) Failed: XCTAssertEqual failed: (”[]”) is not equal to (”[WeatherClient.WeatherResponse.ConsolidatedWeather(applicableDate: 2001-01-01 00:00:00 +0000, id: 1, maxTemp: 30.0, minTemp: 20.0, theTemp: 25.0)]”) We are getting these failures because if Core Location doesn’t report a location back to us, then we won’t request the location from the API, and if we don’t do that then we won’t request the weather for that location.

19:34

We are seeing the cascading effects of this one dependency misbehaving, and this gives us more confidence that we aren’t merely testing our mocks but that we are truly testing how our view model interacts with our dependencies. It is showing that as long as our dependencies are well-behaved in real life, then we can prove that our application will work with those dependencies in the way we expect. And that is really strong.

20:17

Now of course this isn’t testing literally how your code will behave in the real world because it is using mocked dependencies. The only way to accomplish that would be to run your application on a real device in a real environment. That is really powerful to do, but also very difficult to execute and get right in an automated way. The unit tests we are writing get us very close to real life behavior with essentially no infrastructure work, other than being conscious of how we design our dependencies.

20:48

OK, with that said, let’s get our tests passing again by putting in the authorized location client: let viewModel = AppViewModel( locationClient: .authorized, pathMonitorClient: .satisfied, weatherClient: WeatherClient( weather: { _ in Just(moderateWeather).setFailureType(to: Error.self).eraseToAnyPublisher }, searchLocations: { _ in Just([brooklyn]).setFailureType(to: Error.self).eraseToAnyPublisher } ) ) Ergonomic mocking

21:04

We’ve now seen that our first test is actually testing quite a lot. It’s goes through our entire stack of dependencies in order to produce the results we assert against.

21:40

We’re going to write a few more tests to show even more impressive things, but before doing that I think we can improve this test a bit, because it requires quite a bit of setup work. In order to motivate ourselves and our team to write tests, we should eliminate as much friction as possible, and luckily we can do so with just a little bit of upfront work.

22:06

If we take another look at our test in its entirety: func testBasics() { let moderateWeather = WeatherResponse( consolidatedWeather: [ .init( applicableDate: Date(timeIntervalSinceReferenceDate: 0), id: 1, maxTemp: 30, minTemp: 20, theTemp: 25 ), ] ) let brooklyn = Location(woeid: 1, title: "Brooklyn") let viewModel = AppViewModel( locationClient: .authorized, pathMonitorClient: .satisfied, weatherClient: WeatherClient( weather: { _ in Just(moderateWeather) .setFailureType(to: Error.self) .eraseToAnyPublisher() }, searchLocations: { _ in Just([brooklyn]) .setFailureType(to: Error.self) .eraseToAnyPublisher() } ) ) XCTAssertEqual(viewModel.currentLocation, brooklyn) XCTAssertTrue(viewModel.isConnected) XCTAssertEqual(viewModel.weatherResults, moderateWeather.consolidatedWeather) }

22:17

We see that we have 28 lines of setup for just 3 lines of assertions. It’s a simple test case but it doesn’t even all fit on the screen at once!

22:45

A lot of this setup is noise around constructing mock models and mock dependency endpoints. Constructing the weather response takes 11 lines alone. If we had to repeat these 11 lines in every test we write it will quickly become tiring.

22:59

Ideally mock data should very lightweight to construct and to customize, so maybe we can extract these values out as static members that can be plucked out of air quite easily, and shared over many tests.

23:16

For example we can extend WeatherResponse with a “moderate weather” mock containing the data we are defining in the test case: extension WeatherResponse { static let moderateWeather = Self( consolidatedWeather: [ .init( applicableDate: Date(timeIntervalSinceReferenceDate: 0), id: 1, maxTemp: 30, minTemp: 20, theTemp: 25 ), ] ) }

23:30

And now we can eliminate those 11 lines from the test and instead use the value inline. weather: { _ in Just(.moderateWeather) .setFailureType(to: Error.self) .eraseToAnyPublisher() }, … XCTAssertEqual( viewModel.weatherResults, WeatherResponse.moderateWeather.consolidatedWeather )

23:40

We can do the same with the location. While the model struct is small enough to fit on one line right now, it may grow in the future if we need more data from the API. And if we do, it will also be nice to be able to update it in just one place, rather than having to update every single test that needs a location. extension Location { static let brooklyn = Self( woeid: 1, title: "Brooklyn" ) }

24:00

And we can use this mock in our test, as well. searchLocations: { _ in Just([.brooklyn]) .setFailureType(to: Error.self) .eraseToAnyPublisher() } … XCTAssertEqual(viewModel.currentLocation, Location.brooklyn)

24:12

There is one more bit of noise I’d like to clean up, and that’s where we create mock publishers for our dependency endpoints: weather: { _ in Just(.moderateWeather) .setFailureType(to: Error.self) .eraseToAnyPublisher() }, searchLocations: { _ in Just([.brooklyn]) .setFailureType(to: Error.self) .eraseToAnyPublisher() }

24:21

This is pretty verbose when all we really care about is that we’re packaging a couple values up into AnyPublisher s. It might not be so quite bad if we could do just: Just(.moderateWeather).eraseToAnyPublisher()

24:39

But for whatever reason, the Just publisher locks its failure type to Never , which makes it less useful for swapping in mock values as we are here.

24:50

Our controlled dependencies must always return publishers as AnyPublisher s, because this is precisely what gives us the ability to use live publishers in our application and mock publishers in our previews and tests.

25:04

We’ll be creating these mock publishers a lot in our tests, so it might be useful to have a helper that eliminates some of this noise. We can define it as a convenience initializer on the AnyPublisher type, because this is the concrete publisher we are working with. It will take a single value to return immediately, and we’ll do the Just - setFailureType - eraseToAnyPublisher dance under the hood. extension AnyPublisher { init(_ value: Output) { self = Just(value) .setFailureType(to: Failure.self) .eraseToAnyPublisher() } }

25:47

We can update each endpoint: weather: { _ in .init(.moderateWeather) }, searchLocations: { _ in .init([.brooklyn]) }

26:04

And this reads very nicely. They are hyper-focussed on describing the data they return without any extra noise.

26:15

Our test is now down to a readable 10 lines of code that all fits on the screen at once and where most of the noise has been pushed out to a reusable helpers. Testing reachability

26:28

So let’s write another test, this time with our new helpers at the ready.

26:33

We have good coverage on the happy path of being in a connected, location-authorized state and seen how it leads our feature to interact with the weather client. Let’s now get some coverage on how it interacts with the path monitor client. We’ll start by testing the flow of entering the feature in a disconnected state. func testDisconnected() { }

26:51

First we can construct a new view model and supply it its dependencies. We will still use an “authorized” location client again, but this time we will use an “unsatisfied” path monitor. let viewModel = AppViewModel( locationClient: .authorized, pathMonitorClient: .unsatisfied, weatherClient: <#WeatherClient#> )

27:15

We could also create this dependency from scratch, since we will be asserting directly against the state it affects, but the dependency is simple enough for now that we will just use our mock instead.

27:33

We don’t expect any interaction with the weather client at all, so we could pass along any value, really. Maybe our “happy path” mock: weatherClient: .happyPath

27:46

But even better, we can construct a mock that invokes fatal errors for each of its endpoints, which will prove that none of them are called.

27:53

This is the second time we are seeing that this kind of fatal erroring dependency can be useful, so rather than define these fatal errors inline, we could maybe cook up another static value that can be used in any test. extension WeatherClient { static let unimplemented = Self( weather: { _ in fatalError() }, searchLocations: { _ in fatalError() } ) }

28:30

And then we can complete our test by passing along our fatal mock dependency and asserting against our view model state. This time we expect it to be disconnected, and for the current location to be nil and weather results to be empty, because we shouldn’t be making API requests while we’re offline. func testDisconnected() { let viewModel = AppViewModel( locationClient: .authorized, pathMonitorClient: .unsatisfied, weatherClient: .unimplemented ) XCTAssertEqual(viewModel.currentLocation, nil) XCTAssertEqual(viewModel.isConnected, false) XCTAssertEqual(viewModel.weatherResults, []) }

29:30

This test is even more succinct than the last. But when we run it…

29:33

…it crashes: searchLocations: { _ in fatalError() }

29:39

Despite being disconnected from the internet we have for some reason made an API request. We have actually uncovered a bug here.

29:48

This is happening because as long as our view model is able to request the current location, we immediately fire off a search locations request: case let .didUpdateLocations(locations): guard let location = locations.first else { return } self.searchLocationCancellable = self.weatherClient .searchLocations(location.coordinate) .sink( receiveCompletion: { _ in }, receiveValue: { [weak self] locations in self?.currentLocation = locations.first } )

30:09

We should update are guard to also check that we are in a connected state. guard self.isConnected, let location = locations.first else { return }

30:20

And now the test passes.

30:27

So this test was quite a bit easier to write than the last, and we were able to introduce another bit of reusable code along the way: an unimplemented weather client that proves we don’t attempt to hit the API by fatal erroring in each of its endpoints. And with it we even uncovered and fixed a bug.

30:45

Let’s kick things up a notch by testing connectivity changes over time. We can test what it means for our view model to go from a connected state to disconnected and then back to connected again, as there is a lot of logic tied up in this series of steps.

31:06

We can get a test case going and stub out another view model. func testPathUpdates() { let viewModel = AppViewModel( locationClient: <#LocationClient#>, pathMonitorClient: <#PathMonitorClient#>, weatherClient: <#WeatherClient#> ) }

31:18

The locationClient should be “authorized” because we want to test the happy path when we are connected. locationClient: .authorizedWhenInUse,

31:25

The path monitor client we want to create from scratch, because we will be asserting directly against it over time. Its initializer takes a single publisher. pathMonitorClient: PathMonitorClient( pathUpdatePublisher: <#AnyPublisher<NetworkPath, Never>#> ),

31:38

To supply this publisher we will create a subject that can be sent many path updates over the duration of the test. let pathUpdateSubject = PassthroughSubject<NetworkPath, Never>()

31:52

We just need to erase it to an AnyPublisher before passing it along. pathUpdatePublisher: pathUpdateSubject.eraseToAnyPublisher()

31:57

And we expect the weather client to be exercised when the view model is in a connected state, so we can create one with well-defined mock data that we can assert against, as we did in our first test. weatherClient: WeatherClient( weather: { _ in .init(.moderateWeather) }, searchLocations: { _ in .init([.brooklyn]) } )

32:14

Before we make a single assertion we need to send the subject its start state, which will simulate the path a path monitor emits when it starts. We’ll send a “satisfied” path to simulate starting in a connected state. pathUpdateSubject.send(NetworkPath(status: .satisfied))

32:30

Our first set of assertions against view model state will be the same as our first test, where we expect to be in a connected state with a current location and some weather results. XCTAssertEqual(viewModel.currentLocation, .brooklyn) XCTAssertEqual(viewModel.isConnected, true) XCTAssertEqual( viewModel.weatherResults, WeatherResponse.moderateWeather.consolidatedWeather)

33:02

But this time, because we have injected a subject for the path monitor client, we can simulate the network flipping into a disconnected state. pathUpdateSubject.send(NetworkPath(status: .unsatisfied))

33:28

And now we expect isConnected to have gone false and for the weather results to be cleared. The current location should remain the same because we do not nil it out when we go offline: XCTAssertEqual(viewModel.currentLocation, .brooklyn) XCTAssertEqual(viewModel.isConnected, false) XCTAssertEqual(viewModel.weatherResults, [])

33:50

This passes, as well!

34:02

Finally we will assert what happens when the network becomes available again, because we expect that the view model should refresh its weather automatically. pathUpdateSubject.send(NetworkPath(status: .satisfied)) XCTAssertTrue(viewModel.isConnected) XCTAssertEqual(viewModel.currentLocation, .brooklyn) XCTAssertEqual( viewModel.weatherResults, WeatherResponse.moderateWeather.consolidatedWeather)

34:26

And it does.

34:34

So with very little work we were able to test the full lifecycle of what happens when the weather feature loses and regains its network connection, and it was all made possible and easy because of how we’ve designed our dependencies in the first place. Testing core location

35:28

Let’s get a final bit of coverage on our feature, because we still haven’t captured the flow where the user has not yet granted location authorization. It would be good to test the entire flow of starting in a “not authorized” state, tapping the location button, authorizing the application, and having it immediately fire off the API requests needed to fetch and display the weather.

36:05

Let’s get a test in place and start filling in our view model’s dependencies. func testLocationAuthorization() { let viewModel = AppViewModel( locationClient: <#LocationClient#>, pathMonitorClient: <#PathMonitorClient#>, weatherClient: <#WeatherClient#> ) }

36:23

This time we will construct a location manager from scratch so that we can control the series of steps for authorization. locationClient: LocationClient( authorizationStatus: <#() -> CLAuthorizationStatus#>, requestWhenInUseAuthorization: <#() -> Void#>, requestLocation: <#() -> Void#> delegate: <#AnyPublisher<LocationClient.DelegateEvent, Never>#> ),

36:41

Authorization status should start in a “not determined” state and should flip to “authorized” when the view model requests authorization. In order to change this value over time we can introduce some local, mutable state and capture it in the dependency. var authorizationStatus = CLAuthorizationStatus.notDetermined … authorizationStatus: { authorizationStatus },

37:41

Next, when the dependency’s authorization is requested, we will update this local variable and notify the delegate that the authorization did change, capturing the flow we expect from a CLLocationManager interacting with its delegate. We don’t have a delegate just yet, though. requestWhenInUseAuthorization: { authorizationStatus = .authorizedWhenInUse // .didChangeAuthorization },

38:15

Then, when the location is requested we can feed a mock location to the delegate so that the weather client can be exercised. requestLocation: { // .didUpdateLocations }

38:42

Finally we need a delegate event publisher, so we will introduce a subject that can be sent delegate events over the course of the test. And we can send events to it from our endpoints. let locationDelegateSubject = PassthroughSubject< LocationClient.DelegateEvent, Never >() … requestWhenInUseAuthorization: { authorizationStatus = .authorizedWhenInUse locationDelegateSubject.send( .didChangeAuthorization(authorizationStatus) ) }, requestLocation: { locationDelegateSubject.send(.didUpdateLocations([CLLocation()])) }, delegate: locationDelegateSubject.eraseToAnyPublisher(), When we feed a location to didUpdateLocations we aren’t going to worry about any specific data on it, but in the future we could feed a specific coordinate to test that it’s passed along to the weather client as we expect.

40:26

Okay, so the location manager took quite a bit more work to deliberately control, which is not surprising because it is also our most complex dependency by far. Still, despite there being 4 endpoints to mock and needing to introduce a passthrough subject and some local mutable state, we have set ourselves up to assert against a quite complex state machine in subtle ways.

40:53

Let’s fill in our remaining dependencies so we can make some assertions. We want to test the full happy path of authorization, from tapping the button to loading the weather, so we want a connected path monitor client. pathMonitorClient: .satisfied,

41:03

And we will mock out our weather client dependency with data we can assert against. weatherClient: WeatherClient( weather: { _ in .init(.moderateWeather) }, searchLocations: { _ in .init([.brooklyn]) } )

41:13

We can immediately make some assertions that we are in a connected state, but this time, because we have not authorized the location client, we expect the current location to be nil and so the weather results to be empty. XCTAssertEqual(viewModel.currentLocation, nil) XCTAssertEqual(viewModel.isConnected, true) XCTAssertEqual(viewModel.weatherResults, [])

41:48

And now, we can tell the view model its location button was tapped so that it can fire off the series of events we mocked in our location client. viewModel.locationButtonTapped()

42:11

That one interaction should set off a chain of events that ultimately result in the population of the user’s current location and weather results. XCTAssertEqual(viewModel.currentLocation, .brooklyn) XCTAssertEqual(viewModel.isConnected, true) XCTAssertEqual( viewModel.weatherResults, WeatherResponse.moderateWeather.consolidatedWeather )

42:46

And this passes. And remember that the only way for the current location and weather results to become populated was for all of these dependencies to be exercised:

43:06

We should stop and appreciate the complexity of the flow this test captures. In it: We load the feature, which checks to see if it is authorized to request the location. It is not, so it takes no further action. We tap the location button, which checks authorization and sees we’re in a “not determined state,” then requests authorization, then we simulate the idea of the use granting us access to location which triggers a location search request, and uses the response to make a second request for the local weather

43:51

This test is exercising multiple parts of our view model at once, and exercising how multiple dependencies interact with each other. So we are getting a ton of coverage here and in not a ton of code, and it’s all thanks to the way we designed the dependencies.

44:06

This is the happy path, though. What about the unhappy path, where a user denies permission? We can copy and paste the existing test and make a few changes: func testLocationAuthorizationDenied() { let authorizationStatus = CLAuthorizationStatus.notDetermined let locationDelegateSubject = PassthroughSubject< LocationClient.DelegateEvent, Never >() let viewModel = AppViewModel( locationClient: LocationClient( authorizationStatus: { authorizationStatus }, requestWhenInUseAuthorization: { authorizationStatus = .denied locationDelegateSubject.send( .didChangeAuthorization(authorizationStatus) ) }, requestLocation: { locationDelegateSubject.send( .didUpdateLocations([CLLocation()]) ) }, delegate: locationDelegateSubject.eraseToAnyPublisher() ), pathMonitorClient: .satisfied, weatherClient: WeatherClient( weather: { _ in .init(.moderateWeather) }, searchLocations: { _ in .init([.brooklyn]) } ) ) XCTAssertEqual(viewModel.currentLocation, nil) XCTAssertEqual(viewModel.isConnected, true) XCTAssertEqual(viewModel.weatherResults, []) viewModel.locationButtonTapped() XCTAssertEqual(viewModel.currentLocation, nil) XCTAssertEqual(viewModel.isConnected, true) XCTAssertEqual(viewModel.weatherResults, []) } Namely, the dependency now returns .denied , and we have updated our expectations to leave the location nil and weather results empty.

44:46

So this is showing us that because we denied access our view model is not requesting any location data. And to prove this we can fatal error in the endpoint. requestLocation: { fatalError() },

45:17

Now it’s a bit of a bummer that we’re asserting on the exact same information before and after we hit the location button, but as soon as we add proper error handling to this scenario this is where we would test that some alert state was set in our view model. // XCTAssertEqual(viewModel.authorizationAlert, nil) viewModel.locationButtonTapped() // XCTAssertEqual( // viewModel.authorizationAlert, // "Please give us location access." // ) Conclusion

46:13

We have now built a pretty thorough test suite of our application that exercises a lot of its functionality and captures complex interactions over layers of dependencies, and we were able to do in a short amount of time without much friction.

46:35

Had we approached dependencies in the traditional, protocol-oriented way, we would have defined protocols for URLSession , NWPathMonitor , and CLLocationManager that capture the endpoints we call in our application. And then whenever we want to control a dependency to be in a certain state, to debug something, or for a preview or test, you would need go through the ceremony of creating a custom conformance and considering each and every endpoint.

47:24

And there are even more benefits to designing dependencies in this way, and we have some exercises to explore this as well as future episodes to go more in depth. But here’s a small sample:

47:33

You can easily cook up new dependencies that have some of its endpoints talking to live clients in the real world, and some of its endpoints mocked out. For example, what if you wanted to make a build of your application that used the standard CLLocationManager dependency for the most part, but once a location was actually delivered it would us a hard coded CLLocation instead of whatever Core Location reported. That would allow you to instantly see how your application behaves when run halfway across the world.

48:20

You can write functions that take a dependency as input and return a dependency as output, allowing you to enhance an existing dependency with extra functionality. For example, it’s possible to write a function that transforms any existing WeatherClient into a new one, except where all of its endpoints are take a few seconds longer to execute. This would allow you to instantly see how your app behaves when it has a slow internet connection.

49:05

We can even generalize the previous two ideas by cooking up a debug screen that is embedded in your application that allows you to tweak and override any dependency in your app. Your beta tests could play around with various dependencies, such as faking their location or simulating a slow internet connection. That could help you track down bugs that are related to edge cases your application can get into based on the dependencies you are using.

49:30

And so this is the point of being conscious of when we are using a dependency, why we would want to control the dependency, and how to properly design the dependency so that we can make use of it without accidentally making our application more complex than it needs to be. Because remember, there’s “accidental” complexity and then there’s “essential” complexity, and while dependencies are essentially complex, the way we consume them in our applications can introduce accidental complexity on top when we don’t control them or design them.

50:22

We have shown that by scrapping the protocols and focusing on simple data types to encapsulate a dependencies functionality, we are able to come up with something that is super easy to control, super easy to test, and infinitely transformable so that there is no limit to what kind of cool things you can do with it. Until next time… Downloads Sample code 0114-designing-dependencies-pt5 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .