EP 83 · Testable State Management · Dec 2, 2019 ·Members

Video #83: Testable State Management: Effects

smart_display

Loading stream…

Video #83: Testable State Management: Effects

Episode: Video #83 Date: Dec 2, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep83-testable-state-management-effects

Episode thumbnail

Description

Side effects are by far the hardest thing to test in an application. They speak to the outside world and they tend to be sprinkled around to get the job done. However, we can get broad test coverage of our reducer’s effects with very little work, and it will all be thanks to a simple technique we covered in the past.

Video

Cloudflare Stream video ID: d38094c20fda028ab01723591aed3286 Local file: video_83_testable-state-management-effects.mp4 *(download with --video 83)*

References

Transcript

0:05

This is basically an integration test for reducers. We are testing multiple layers of features, understanding how they interact with each other, and making assertions that they play nicely together. This is pretty huge! Again this is only a toy app, but in a large application you would be able to test that lots of tiny, reusable components continue working properly when they are plugged together. This is already powerful, and we haven’t even discussed effects yet. Testing effects

0:44

We’ve now seen that reducers as pure functions are incredibly easy to test. We start by constructing some mutable app state that describes the state we are currently in, then we apply our reducer to this state with a particular action to describe the next state of our application, and then we finish by asserting that the new state equals what we expect.

0:57

Effects, on the other hand, don’t seem to be very testable. When we call a reducer, we can assert that it produced no effects, or we can assert that it produced some number of effects, but we can’t assert that a specific effect was produced. This is because the Effect type is a mere wrapper around a function, so we have no notion of equating them. To work around this we manually ran the actions that we expected the effects to produce so that we could verify that the reducer is doing its job with those responses. However, doing that manual work is fragile. We may forget to do it, which means we aren’t testing effects at all, or we could construct those effect actions incorrectly, which would mean we are testing something that wouldn’t really happen in the real world.

1:20

Maybe we could run these effects to test them, but effects are rarely testable, and this is one of the reasons why we’ve separated them from the reducer’s pure business logic. For example, our “save” and “load” effects are very simple, yet testing them introduces a number of complications. If we run a “save” effect, we write JSON to disk somewhere, and to assert that the right data was written correctly, the test would need to know where the file lives, or we would need to test the “load” effect at the same time. The test would definitely need to know where the file lives if we want to clean up after it and delete whatever file it creates. Another problem with testing these effects is that they interact with the file system. In some environments, reading and writing to disk may fail depending on file permissions or disk space.

1:48

Testing effects gets even more complicated depending on how complicated the effect is. Tapping the “nth prime button” produces an asynchronous effect that makes a request to Wolfram Alpha, which means testing it will require that the test machine has a network connection and that Wolfram Alpha is up and running as expected. Such a test has to deal with the asynchronous nature of the effect, where it has to wait for a response, making the test suite much slower in the process. We’re at the mercy of a great number of things for such a test to pass, and we’ve introduced a flaky, slow test that may occasionally break CI.

2:05

So how can we test effects in this architecture? What can we do to control these various effects to ensure that certain data is handed to the effects, and certain results are handed back to the reducer?

2:11

Well in the past we have covered just this! A long time ago, just in our 16th episode , we showed an approach to lightweight dependency injection that we called the “Environment.” Let’s see if we can use our knowledge of “Environment” to make effects testable. Recap: the environment

2:36

In a nutshell, the environment allowed us to bundle up all of our application’s dependencies in a single data type with mutable fields that made it super easy to swap out mock implementations for live implementations, and vice versa. This allowed us to exercise all the edge cases and unhappy paths in our code, which was great not only for writing tests, but for running code in playgrounds.

3:04

We are going to try out the environment idea to control the side effects in our application. We’ll start with favorite primes screen, which has the simplest effects of saving and loading favorite primes. We’ll introduce an environment struct, which will eventually hold all the side-effecting dependencies we need for this module: struct Environment { }

3:27

Just to jog our memory of how dependencies get added to this environment, suppose for a moment that this module needed access to the current date. This is definitely a side effect since every time you create a date the time has changed. However, instead of allowing ourselves to call out to the date initializer directly from our code, we will add it to our environment: struct Environment { var date: () -> Date }

3:52

Further, we can extend the environment with default, live implementations of these dependencies. extension Environment { static let live = Environment( date: Date.init ) }

4:08

And then we’ll have a global environment in the module so that any time we want access to the date we just access the one in the current environment: var Current = Environment()

4:22

What this means is that when we want to get a date value, we should force ourselves to go through the current environment rather than allowing ourselves to reach out to the uncontrolled date initializer. Current.date()

4:39

And because this is a mutable property, we can swap out our implementation in a much more controllable way. For instance, we can create a mock version of the environment that just has the date function always return the same date: extension Environment { static let mock = Environment( date: { Date(timeIntervalSince1970: 1234567890) } ) }

5:06

And then in tests we will just swap out our live environment with the mock one: Current = .mock This allows us to control this dependency in a lightweight way when we want, such as in tests and in playgrounds, while the production app will use the live version of these dependencies.

5:23

The date dependency is pretty simple, but in our episode on dependency injection we also showed a more complicated dependency: a GitHub API client: struct GitHubClient { var fetchRepos: (@escaping (Result<[Repo], Error>) -> Void) -> Void struct Repo: Decodable { var archived: Bool var description: String? var htmlUrl: URL var name: String var pushedAt: Date? } }

5:51

Again, it’s expressed as just a simple struct with some mutable fields so that we can easily swap out implementations for mocks.

5:58

And we 100% understand how uncomfortable this code might might make some of our viewers feel. For one thing, we have a variable whose identifier is capitalized. And for another we have a mutable global variable. However, these dependencies should only be swapped out in playgrounds and tests. In a production app the dependencies should be created in the environment and then left alone. You could even create lint rules to make sure the environment is never modified outside of a playground or test. You can also make use of the fact that Environment is a value type and force it to be fully immutable in production: #if DEBUG var Current = Environment() #else let Current = Environment() #endif

7:00

And that’s the environment in a nutshell! You create a struct called Environment with a bunch of mutable fields describing your app’s dependencies. You create a live version that points to the live versions of all your dependencies. You create a mock version that stubs out those various endpoints with simple, controlled defaults. And then in your tests and playgrounds, you can use the mock version and further swap out other mock scenarios, while in your application you will use the live version.

7:24

If you still feel uncomfortable with this, we highly recommend you watch our 16th episode on the topic where hopefully we can convince you that this style of dependency management comes with lots of benefits and greatly reduces the boilerplate and complexity of existing solutions.

7:36

Let’s now try to apply the environment to control our application’s effects. Controlling the favorite primes save effect

7:44

Now that we remember how the environment works, let’s create one to control the save and load effects in this module. They are currently little private helpers which wrap the effectful logic in an Effect type: private func saveEffect( favoritePrimes: [Int] ) -> Effect<FavoritePrimesAction> { return .fireAndForget { let data = try! JSONEncoder().encode(favoritePrimes) let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) let favoritePrimesUrl = documentsUrl .appendingPathComponent("favorite-primes.json") try! data.write(to: favoritePrimesUrl) } } private let loadEffect = Effect<FavoritePrimesAction>.sync { let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) let favoritePrimesUrl = documentsUrl .appendingPathComponent("favorite-primes.json") guard let data = try? Data(contentsOf: favoritePrimesUrl), let favoritePrimes = try? JSONDecoder() .decode([Int].self, from: data) else { return nil } return .loadedFavoritePrimes(favoritePrimes) } .eraseToEffect()

7:58

And these live implementations of the effects are returned from the reducer: case .saveButtonTapped: return [saveEffect(favoritePrimes: state)] case .loadButtonTapped: return [loadEffect]

8:04

We want to capture these effects in a dependency that can be put in the environment. Let’s introduce a brand new environment for the favorite primes module. struct FavoritePrimesEnvironment { }

8:04

So what goes in here? We could add fields for both save and load effects directly to this struct, but because they’re related, l8et’s take some inspiration from the GitHubClient and create a FileClient that will hold the effects for saving and loading: struct FileClient { }

8:39

Each effect will be a field in this struct: struct FileClient { // var load: // var save: }

8:38

Let’s start with the load effect. At its core, all loading is concerned with is producing an array of integers that represent favorite primes, so we might be tempted to do the following: struct FileClient { var load: () -> [Int]? // var save: }

8:46

There are two things wrong with this:

8:48

First, the type is giving no indication that an effect will happen. If we invoke this we know that it is going to reach out into the world to access data on disk, and we want that represented in the type.

8:58

Second, this effect is highly specific to our one use case of literally loading an array of integers from a file named favorite-primes.json . We could generalize this so that a file name is passed in and so that we just get Data back, and then we can do the JSON decoding ourselves.

9:13

It’s easy enough to fix these problems. We can first introduce a String argument for the file name, and then we can generalize the optional array of integers returned for optional data. struct FileClient { var load: (String) -> Effect<Data?> // var save: }

9:27

The save field can be handled similarly, but it can take the name of the file to save to as well as the data it needs to save. It also needs to return an effect, but it’s not clear what the effect should be generic over: var save: (String, Data) -> Effect<???>

9:41

Remember that this is a fire-and-forget effect. It just needs to do the job of saving some data to disk, and doesn’t need to send any data back into the system. One thing we could do is hard-code the FavoritePrimesAction into this effect: var save: (String, Data) -> Effect<FavoritePrimesAction>

9:54

However, this unnecessarily couples this file client directly to this module. It prevents us from extracting out this client into its own module someday and using it in other screens or applications that don’t care about this favorite primes screen.

10:07

We could decouple the type of the effect from the FileClient by making the FileClient generic. This would allow anyone using the file client to bring their own type: struct FileClient<Action> { … var save: (String, Data) -> Effect<A> }

10:17

But this seems pretty heavyweight, and at the end of the day we don’t even want to be able to allow the save effect to be able to produce actions that are fed back into the system.

10:25

We might even be tempted to just use Void for the type of the effect, since that represents a piece of data that carries no semantics or significance: struct FileClient { … var save: (String, Data) -> Effect<Void> }

10:35

Still this is not right because this would allow the save effect to send a void value back into the system.

10:41

All of these little false starts that we are exploring are pointing at something very important. We want to represent something very specific in the type of this effect. We want the idea of an effect that can do some work, but can never produce a value. It should have no ability to send an action back into the store.

10:58

There is a type that allows us to do just that: Never . var save: (String, Data) -> Effect<Never>

11:01

Never is the so-called “uninhabited” type. It’s an enum with zero cases and is defined in the Swift standard library. public enum Never { }

11:08

It’s impossible to construct a value of the Never type. And because it’s impossible to construct a value, it is also impossible for this effect publisher to produce an emission of the Never type. This is compile time verification of the property that we want this effect to satisfy.

11:25

This also makes for really great documentation of our APIs. If you ever encounter an Effect<Never> type you know without even looking at the implementation that it can never produce a value, and so it must be a fire-and-forget effect.

11:37

Now that we’ve got our dependency described as a simple data type, we can create a live version of it for use in production. We can define it as a static on the FileClient type: extension FileClient { static let live = Self( load: <#(String) -> Effect<Data?>#>, save: <#(String, Data) -> Effect<Never>#> ) }

11:53

We can basically copy and paste everything from the current effects over to these closures, making minimal changes: extension FileClient { static let live = Self( load: { fileName in .sync { let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) let favoritePrimesUrl = documentsUrl .appendingPathComponent(fileName) return try? Data(contentsOf: favoritePrimesUrl) } }, save: { fileName, data in .fireAndForget { let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) let favoritePrimesUrl = documentsUrl .appendingPathComponent(fileName) try! data.write(to: favoritePrimesUrl) } } ) }

13:21

And now we can add this dependency to our environment: struct FavoritePrimesEnvironment { var fileClient: FileClient }

13:28

And now we can define a live version of the environment, which uses the live implementation of the file client: extension FavoritePrimesEnvironment { static let live = FavoritePrimesEnvironment(fileClient: .live) }

13:44

Finally, we want to instantiate a live instance of the environment using our capital Current : var Current = FavoritePrimesEnvironment.live

13:56

With the environment in place we just need to refactor our reducer so that it no longer constructs its effects directly, but instead only uses the Current environment.

13:58

Let’s start with the save effect: case .saveButtonTapped: return [saveEffect(favoritePrimes: state)]

14:09

Instead of invoking this local, private effect, we can use the new effect inside our environment: Current.fileClient.save

14:17

The file name is the same thing we’ve been using this whole time: Current.fileClient .save("favorite-primes.json", <#???#>)

14:24

To give this function data we must do the JSON encoding right in the reducer. This is a perfectly fine thing to do since JSON encoding is a pure operation: Current.fileClient.save( "favorite-primes.json", try! JSONEncoder().encode(state) )

14:37

Now this doesn’t currently compile because this returns an Effect<Never> but we need to return an Effect<FavoritePrimesAction> : Cannot convert value of type ‘Effect<Never>’ to expected element type ‘Effect<FavoritePrimesAction>’

14:47

We did all that talking a moment ago about how it must be the right thing to return an Effect<Never> to represent a fire-and-forget effect, but now it seems to be giving us problems. How on earth can we transform a Never value into a FavoritePrimesAction value?

14:59

Well, it absolutely is possible, and it’s something we discussed on just our 9th episode on Point-Free when discussing the relationship between Swift’s type system and algebra. Using algebra as our guiding light we were able to discover that there is actually one implementation of the following signature: // (Never) -> A

15:20

We called it absurd , because it seems pretty absurd: func absurd<A>(_ never: Never) -> A { switch never {} }

15:25

This compiles because our switch has exhaustively handled every case from Never , which is vacuously true since Never has no cases.

15:35

This was our implementation back in the episode , but since then Swift has gotten a bit smarter and we can now even omit the body of this function: func absurd<A>(_ never: Never) -> A {}

15:43

And this is precisely the function we can use to lift our fire-and-forget effect up from the world of Never s up to the world of our reducer’s action: Current.fileClient .save("favorite-primes.json", try! JSONEncoder().encode(state)) .map(absurd)

15:58

And now this isn’t compiling because we gotta make sure to erase the publisher type back to the effect type: Current.fileClient .save("favorite-primes.json", try! JSONEncoder().encode(state)) .map(absurd) .eraseToEffect()

16:03

And now this compiles. We can even bundle this little absurd dance in a custom operator so that we have a nice name for it: extension Publisher where Output == Never, Failure == Never { func fireAndForget<A>() -> Effect<A> { return self.map(absurd).eraseToEffect() } }

16:39

And now we can simply do: Current.fileClient .save("favorite-primes.json", try! JSONEncoder().encode(state)) .fireAndForget()

16:46

This reads really nicely. It clearly states that this is a fire-and-forget effect, and it allows us to upcast the Never type to any type that we need to return from this reducer.

16:55

We can now delete the saveEffect we had from before: // private func saveEffect( // favoritePrimes: [Int] // ) -> Effect<FavoritePrimesAction> { // return .fireAndForget { // let data = try! JSONEncoder().encode(favoritePrimes) // let documentsPath = NSSearchPathForDirectoriesInDomains( // .documentDirectory, .userDomainMask, true // )[0] // let documentsUrl = URL(fileURLWithPath: documentsPath) // let favoritePrimesUrl = documentsUrl // .appendingPathComponent("favorite-primes.json") // try! data.write(to: favoritePrimesUrl) // } // } Controlling the favorite primes load effect

17:06

One effect down, one to go. We were able to extract our effects to an environment so that they were in a place where their live implementations can be swapped out for mock implementations, but we’ve only handled a single fire-and-forget effect.

17:19

Next we can change our load effect to use the environment: case .loadButtonTapped: return [ loadEffect .compactMap { $0 } .eraseToEffect() ]

17:39

We can start by invoking the endpoint on our environment: Current.fileClient.load("favorite-primes.json")

18:02

This gives an effect of Data? . We want to decode this from JSON, and luckily there’s a helper on Combine publishers that do just that: Current.fileClient.load("favorite-primes.json") .decode(type: [Int].self, decoder: JSONDecoder())

18:24

However, this doesn’t work because decode expects a publisher of honest Data , yet we are giving it optional Data . We can again use compactMap to discard the nil s: Current.fileClient .load("favorite-primes.json") .compactMap { $0 } .decode(type: [Int].self, decoder: JSONDecoder())

18:43

Now technically this gives us a publisher that is capable of failing because the process of decoding from JSON can fail. But our Effect publisher is one that is not allowed to fail, because we must explicitly handle any failure directly in our reducer.

18:56

We can address this problem with the catch method on publishers, which allows you to intercept any errors that the publisher produces and map it to a whole new publisher. In our case we want to completely ignore these errors, so we could just map it to a publisher that completes immediately: Current.fileClient .load("favorite-primes.json") .compactMap { $0 } .decode(type: [Int].self, decoder: JSONDecoder()) .catch { _ in Empty(completeImmediately: true) }

19:39

Now we have a publisher of just a simple array of integers, so all we have to do is map it into our FavoritePrimesAction and erase the publisher to our effect type: Current.fileClient .load("favorite-primes.json") .compactMap { $0 } .decode(type: [Int].self, decoder: JSONDecoder()) .catch { _ in Empty(completeImmediately: true) } .map(FavoritePrimesAction.loadedFavoritePrimes) .eraseToEffect()

20:16

Now this module is compiling, and just like that we have controlled all of the effects in this screen. We can even comment out our old, uncontrollable load effect. Anything that can perform a side effect has been stuffed into this Environment type, which holds the live implementations for the production app but gives us easy access for swapping out implementations when we want.

20:34

To see this, let’s create a mock version to live alongside our live version: extension FavoritePrimesEnvironment { static let mock = FavoritePrimesEnvironment( fileClient: FileClient( load: { _ in Effect<Data?>.sync { try! JSONEncoder().encode([2, 31])) } }, save: { _, _ in .fireAndForget {} } ) ) }

22:01

We can even guard this code inside a debug check so that it is only shipped with debug builds of the app: #if DEBUG … #endif

22:18

And now we have a super easy way to swap out the live version of our environment for the mock version. To show the power of this, let’s update our playground to use the mock environment instead of actually accessing the file system: @testable import FavoritePrimes … Current = .mock PlaygroundPage.current.liveView = UIHostingController( rootView: NavigationView { FavoritePrimesView( store: Store<[Int], FavoritePrimesAction>( initialValue: [2, 3], reducer: favoritePrimesReducer ) ) } )

22:48

If we run the playground and hit load, we will see that the primes 2 and 31 pop in, even though we never hit save. And that’s because the mock load effect hard coded those primes to be loaded. We could even stress test this app by seeing how it deals with loading a super large list of primes: Current = .mock Current.fileClient.load = { _ in Effect.sync { try! JSONEncoder().encode(Array(1...100)) } }

23:23

And just like that we have seen what happens when we try to load 100 numbers into this interface. Of course, they aren’t primes, but that doesn’t really matter for the thing we are trying to test right now. To make this clear, let’s up the size of this array to hold one thousand integers: Current.fileClient.load = { _ in Effect.sync { try! JSONEncoder().encode(Array(1...1000)) } }

23:46

Now when we hit load in the interface we see it will freeze for a few seconds before animating the changes into place. This seems to be a problem with SwiftUI and very large lists, but it’s awesome that we can easily stress test the interface by mocking out effects instead of needing to find a way to put a large JSON file of integers on the disk. Testing the favorite primes save effect

24:19

We now have everything we need to start writing some tests for our effects. Let’s look at what tests we have already written for the favoritePrimesReducer : func testSaveButtonTapped() { var state = [2, 3, 5, 7] let effects = favoritePrimesReducer( state: &state, action: .saveButtonTapped ) XCTAssertEqual(state, [2, 3, 5, 7]) XCTAssertEqual(effects.count, 1) }

24:50

This is doing the simplest test possible, which is that when we tap the save button we do not change the state, but we do emit one effect. To test what that effect does we could run it: _ = effects[0].sink { }

25:09

Remember that by default we are using the live implementation of this effect, which means to understand what this effect did we would need find the file on disk and assert what was saved. As we mentioned before, this is a very fragile thing to do as we might not even have reading or writing permissions on this disk. We would like to completely remove the vagaries of dealing with actual disk storage, which we can do by using our mock environment: Current = .mock

25:45

Now we won’t hit the disk at all. However, that still doesn’t help us test this save effect. All we really want to test is that the save effect was invoked. We will have to trust that the live save effect will do the correct thing as long as it is handed the correct information. To capture this in a mock effect we can simply keep a mutable boolean around that indicates whether or not the save effect was executed, and we will flip it inside the effect: var didSave = false Current.fileClient.save = { _, _ in .fireAndForget { didSave = true } }

26:29

Then we just want to make sure that this boolean value was flipped to true after executing the effect: _ = effects[0].sink { _ in } XCTAssert(didSave)

26:47

If we run the test, it passes, which means we’ve asserted that we the reducer is using the effect we expect it to! This means that as long as we trust that the save effect is doing its job correctly, and it’s a very simple effect, so we can maybe trust that it is, then we are at least getting some coverage on this effect.

27:18

We can go further and make sure that the callback of the effect is never called since this effect is supposed to be a fire-and-forget: _ = effects[0].sink { _ in XCTFail() }

27:34

This is great to have because it guarantees that we have captured the full cycle of this effect, that is, the effect does not produce any more actions that we need to run through the reducer to verify it behaves as expected.

28:01

It’s worth reflecting on what we have just accomplished here. With very little set up and in a very direct fashion we were able to confirm that when the user taps the save button we will not change the state and that a single side effect will be executed which invokes our save dependency and does not emit any other actions to send into the store. That is very broad coverage with very little work.

28:24

However, it’s important clarify that one of the main reasons we are getting this power is due to how our dependency is set up. This style of testing works best when your dependencies are as simple as possible. So simple that you can just simply trust they will do the right thing as long as you give them the right data. And so simple that they contain very little logic on their own. For example, the save and load effects just do the bare minimum of work necessary to get data onto the disk and off the disk. It doesn’t do any data transformations, like JSON decoding, it leaves that to the users of the dependency. And our test exercises those data transformations, while leaving the messiness of interacting with the disk to the dependency.

29:14

And to show that this test is really capturing quite a bit of the behavior of our reducer, let’s pretend we made a really bad copy-pasta mistake and we accidentally used the load effect for the save action: case .saveButtonTapped: return [ Current.fileClient .load("favorite-primes.json") .compactMap { $0 } .decode(type: [Int].self, decoder: JSONDecoder()) .catch { _ in Empty(completeImmediately: true) } .map(FavoritePrimesAction.loadedFavoritePrimes) .eraseToEffect() ]

29:33

Now when we run tests we get two failures: The effect at effects[0] emitted a value, which we know it should not. The didSave flag was not flipped to true, which means the save effect was not invoked.

30:12

And there are other ways of strengthening this test. We could, for example, extract out the data that was sent to the save endpoint and verify that it encoded the correct array of integers. This gives us even more coverage with very little work, but we’ll leave that as an exercise to the viewer.

30:32

This is getting pretty cool, we’re seeing the beginning of what it means to control our effects: as long as we describe our dependencies as simple structs with mutable fields, and as long as we force our reducers to use these dependencies in that struct, we get the ability to swap out a live implementation for a mock one and actually execute these effects to assert they either produced the right value to feed back into the system or didn’t produce anything at all.

31:06

And because effects should contain very little logic and focus on the minimal amount of work they need to do, we don’t need to worry about testing the nitty gritty details of the effects themselves. If an effect merely calls out to Apple’s APIs for loading data from disk, we should be able to hope that’s going to work correctly. What we care about is capturing if a specific effect was invoked, and capturing what data it fed back into the reducer. Testing the favorite primes load effect

31:31

Now that we’ve tested the save effect, let’s test the load effect, which is slightly different in that it needs to feed data back into the reducer.

31:47

Next, let’s look at the next test we previously wrote: func testLoadFavoritePrimesFlow() { var state = [2, 3, 5, 7] var effects = favoritePrimesReducer( state: &state, action: .loadButtonTapped ) XCTAssertEqual(state, [2, 3, 5, 7]) XCTAssertEqual(effects.count, 1) effects = favoritePrimesReducer( state: &state, action: .loadedFavoritePrimes([2, 31]) ) XCTAssertEqual(state, [2, 31]) XCTAssert(effects.isEmpty) }

31:50

This tests that if we tap the load button, the state doesn’t change but an effect is emitted. We don’t know which effect, but we assume it’s the loadedFavoritePrimes effect, and so we run that action in the reducer and assert that the state changed and that no further effects were emitted.

32:07

We want some test coverage on that effect that we ignore, but if we just naively run it we aren’t going to get anything super helpful: _ = effects[0].sink { action in print(action) }

32:26

We don’t want to have to depend on the state of the disk in order to run this test. Luckily we have controlled this effect, so we can bypass the disk entirely and just provide the data directly: Current = .mock Current.fileClient.load = { _ in .sync { try! JSONEncoder().encode([2, 31]) } }

33:17

Now when we run the test the print state executes and we see the action we got was: loadedFavoritePrimes([2, 31])

33:33

This is the data that we actually care about asserting against, because it shows that the effect did some work and came back with this action to feed back into the system. However, if we try to assert directly we have a problem: _ = effects[0].sink { action in XCTAssertEqual(action, .loadedFavoritePrimes([2, 31])) } Global function ‘ XCTAssertEqual(_:_:_:file:line:) ’ requires that ‘FavoritePrimesAction’ conform to ‘Equatable’

33:50

We can’t assert that the action equals something because it isn’t Equatable . That’s easy enough to fix: public enum FavoritePrimesAction: Equatable {

34:03

And now the test compiles, and it passes because the action produced by the effect matches what we expect. But we don’t want to stop here, we next want to take this action and feed it back into the reducer. We can do this by getting a reference to this action outside of the sink and then using it after: var nextAction: FavoritePrimesAction! _ = effects[0].sink { action in XCTAssertEqual(action, .loadedFavoritePrimes([2, 31])) nextAction = action } effects = favoritePrimesReducer(state: &state, action: nextAction) XCTAssertEqual(state, [2, 31]) XCTAssert(effects.isEmpty)

34:48

This is very cool now. We aren’t manually constructing the action that we believe the effect will produce just so that we can feed it back into the store. Instead, we run the effect, assert that it produced the action we expected, feed it back into the reducer, and then assert how that new action changed our state.

35:05

We can even take this a step further by asserting that not only did the effect produce the action we expected, but also that it completed and so will never produce another effect. We can do this by setting up a test expectation, waiting for it after the sink , and then fulfilling the expectation in the completion block: var nextAction: FavoritePrimesAction! let receivedCompletion = self.expectation(description: "receivedCompletion") _ = effects[0].sink( receiveCompletion: { _ in receivedCompletion.fulfill() }, receiveValue: { action in nextAction = action XCTAssertEqual(action, .loadedFavoritePrimes([2, 31])) } ) self.wait(for: [receivedCompletion], timeout: 0)

36:06

And this makes our test even stronger. If we modified our load effect in the reducer to never complete we would get a failure: return [ Current.fileClient .load("favorite-primes.json") .decode(type: [Int].self, decoder: JSONDecoder()) .catch { _ in Empty(completeImmediately: true) } .map(FavoritePrimesAction.loadedFavoritePrimes) .merge(with: Empty(completeImmediately: false)) .eraseToEffect() ] Asynchronous wait failed: Exceeded timeout of 0 seconds, with unfulfilled expectations: “receivedCompletion”.

36:22

The fact that we are capturing this is amazing. It means we are proving that there are no future actions being emitted that we might be accidentally forgetting about.

36:38

Technically we could even make this stronger, because right now we aren’t asserting that this effect emits only one single time. It could potentially have emitted many times and this test would still pass. For example, if we sneak this into our load effect in the reducer: .merge( with: Just(FavoritePrimesAction.loadedFavoritePrimes([2, 31])) )

36:57

Everything still passes, but clearly this is very different behavior. So we still aren’t capture the full story of effects, but we are getting a lot of it. We will soon be able to capture even more, but before we do that let’s finish controller and testing the rest of the side effects in our application.

37:17

But before doing that, let’s clean up this test suite slightly by setting up our default mock environment in the test’s setUp method, that way each test case will start with a fresh mock environment which they can customize however they want: override func setUp() { super.setUp() Current = .mock } Controlling the counter effect

37:51

This has been pretty illuminating so far. Even though the save and load effects don’t seem very complicated, we were able to test a lot about them. Not only could we test that the save effect was doing its work, we could also test that it never produced another action. And not only could we test that the load effect did its job, we were able to assert that it did the job we expected it to and fed the correct action back to the system. We’re getting tons of test coverage with very little work.

38:34

Now that all of the side effects are controlled and testable in the FavoritePrimes module, let’s turn our attention to the Counter module. It only has one effect, which is the network request that hits the Wolfram Alpha API. Previously this was the most difficult for us to get a handle on because it was asynchronous and it took quite a bit of work to understand how it fits into our architecture. However, as far as controlling it goes its quite simple.

38:46

The effect we are currently using is this function which computes the “nth prime” given a particular n : func nthPrime(_ n: Int) -> Effect<Int?> { }

38:57

This is the effect we want to control, so let’s create an environment struct and add it to the environment: struct CounterEnvironment { var nthPrime: (Int) -> Effect<Int?> }

39:15

We can also create a live implementation of this environment by just calling out to the current nthPrime effect: extension CounterEnvironment { static let live = Self(nthPrime: Counter.nthPrime) }

39:51

And we can default the current environment to be the live one. var Current = CounterEnvironment.live

39:58

And while we’re here we might as well create the mock version too: #if DEBUG extension CounterEnvironment { static let mock = CounterEnvironment(nthPrime: { _ in .sync { 17 } }) } #endif

40:24

With our environment in place now all we have to do is use the environment’s nthPrime effect instead of using the live one in this module: // nthPrime(state.count) Current.nthPrime(state.count) .map(CounterAction.nthPrimeResponse) .receive(on: DispatchQueue.main) .eraseToEffect()

40:40

And that’s all it takes to fully control the side effects in the module. Quite a bit easier than the FavoritePrimes module. We can already reap the benefits of this by running our counter screen in its playground with all of its effects mocked out: @testable import Counter … Current = .mock PlaygroundPage.current.liveView = UIHostingController( rootView: CounterView( store: Store<CounterViewState, CounterViewAction>( initialValue: CounterViewState( alertNthPrime: nil, count: 0, favoritePrimes: [], isNthPrimeButtonDisabled: false ), reducer: logging(counterViewReducer) ) ) )

41:10

Now when we run the playground and tap the “what is the nth prime?” button we will immediately get a response saying that the prime is 2. That is of course wrong, but it’s amazing that we can test out this functionality without any dependency on the Wolfram Alpha service. That’s great because we could be on an airplane with no internet access, or maybe some day the Wolfram API will be down. None of that matters when you can properly control the side effects in your application. Testing the counter effects

42:25

Now that our counter effects are controlled, let’s test them. We can start by mocking out the environment in the setUp of the test suite so that we make sure we never use live implementations of our dependencies: override func setUp() { super.setUp() Current = .mock }

42:46

The first few tests don’t have any effects, and so there’s nothing to do there: func testIncrTapped() { … XCTAssert(effects.isEmpty) } func testDecrTapped() { … XCTAssert(effects.isEmpty) }

42:57

The next test, testNthPrimeButtonHappyFlow , tests a full user flow in this screen: func testNthPrimeTappedFlow() { var state = CounterViewState( alertNthPrime: nil, count: 7, favoritePrimes: [2, 3], isNthPrimeButtonDisabled: false ) var effects = counterViewReducer( &state, .counter(.nthPrimeButtonTapped) ) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 7, favoritePrimes: [2, 3], isNthPrimeButtonDisabled: true ) ) XCTAssertEqual(effects.count, 1) effects = counterViewReducer( &state, .counter(.nthPrimeResponse(17)) ) XCTAssertEqual( state, CounterViewState( alertNthPrime: PrimeAlert(prime: 17), count: 7, favoritePrimes: [2, 3], isNthPrimeButtonDisabled: false ) ) XCTAssert(effects.isEmpty) effects = counterViewReducer( &state, .counter(.alertDismissButtonTapped) ) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 7, favoritePrimes: [2, 3], isNthPrimeButtonDisabled: false ) ) XCTAssert(effects.isEmpty) }

43:01

When the user taps on the “nth prime” button some state is changed, in particular the isNthPrimeButtonDisabled field is toggled to true , and a single effect is returned, though we currently don’t know anything about the effect.

43:16

Then we simulate the response from the API by feeding in another action into the reducer that we expect from the effect, but we are still missing coverage on the effect. Let’s try repeating what we did for the favorite primes effects by running the effect and see what we can discover about what happened. We can start by calling sink on the effect to get its completion and value events: _ = effects[0].sink( receiveCompletion: { _ in }, receiveValue: { action in } )

43:44

We can start by asserting that the action this effect produced was the nthPrimeResponse like we expect: _ = effects[0].sink( receiveCompletion: { _ in }, receiveValue: { action in XCTAssertEqual(action, .counter(.nthPrimeResponse(3))) } )

43:53

We just need to make sure our actions are equatable. enum CounterViewAction: Equatable { … enum CounterAction: Equatable { … enum PrimeModalAction: Equatable {

44:13

But what number do we expect here? Well, it depends on what we used in our mock. I happen to remember that we used 17 for this mock effect, but why depend on that when we can just overwrite the endpoint with our own mock that is local to just this test: Current.nthPrime = { _ in .sync { 17 } } … effects[0].sink( receiveCompletion: { _ in }, receiveValue: { action in XCTAssertEqual(action, .counter(.nthPrimeResponse(17))) } )

44:54

But, even though this test is looking good, it can be even better. When dealing with the favorite primes effects we also tested that they completed like we expected, and we even captured the action emitted by the effect so that we could feed it back into the reducer. Let’s give that a shot: var nextAction: CounterViewAction! let receivedCompletion = self.expectation( description: "receiveCompletion" ) _ = effects[0].sink( receiveCompletion: { _ in receivedCompletion.fulfill() }, receiveValue: { action in nextAction = action XCTAssertEqual(action, .counter(.nthPrimeResponse(17))) } ) self.wait(for: [receivedCompletion], timeout: 0.1) effects = counterViewReducer(&state, nextAction) Here we: Created an implicitly unwrapped CounterViewAction so that we could capture the action produced by the effect Created an expectation Fulfilled it when we received a completion event from the effect Capture the action when we receive a value Waited for the expectation to be fulfilled And finally sent the action back into the reducer

46:21

If we run this, we get a crash: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value It looks like nextAction is never being set. To see why, let’s hop over to the reducer. Current.nthPrime(state.count) .map(CounterAction.nthPrimeResponse) .receive(on: DispatchQueue.main) .eraseToEffect()

46:39

This effect is doing something none of the previous effects were doing: work on another queue, which requires more time than the zero seconds we currently wait on. self.wait(for: [receivedCompletion], timeout: 0.01)

47:20

The tests pass and now we’re getting test coverage on the effect. This is already pretty incredible. We are literally testing that a reducer produces an effect such that when run produces the action we expect. And then when feeding that action back into the reducer we can assert that the application’s state is what we expect.

47:54

But we can even go further. Now that we control this effect we can start testing some of the edge cases and unhappy paths. For example, we can simulate what happens when the Wolfram Alpha API has a problem, which means the effect returns nil : func testNthPrimeButtonUnhappyFlow() { Current.nthPrime = { _ in .sync { nil } }

48:09

That’s all it take to simulate the API having an error. After this we can basically copy and paste the previous test, and just make a few small changes: func testNthPrimeButtonUnhappyFlow() { Current.nthPrime = { _ in Effect(value: nil) } var state = CounterViewState( alertNthPrime: nil, count: 7, favoritePrimes: [2, 3], isNthPrimeButtonDisabled: false ) var effects = counterViewReducer( &state, .counter(.nthPrimeButtonTapped) ) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 7, favoritePrimes: [2, 3], isNthPrimeButtonDisabled: true ) ) XCTAssertEqual(effects.count, 1) let receivedCompletion = self.expectation( description: "receivedCompletion" ) var nextAction: CounterViewAction! _ = effects[0].sink( receiveCompletion: { _ in receivedCompletion.fulfill() }, receiveValue: { action in nextAction = action XCTAssertEqual(action, .counter(.nthPrimeResponse(nil))) } ) self.wait(for: [receivedCompletion], timeout: 0.1) effects = counterViewReducer(&state, nextAction) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 7, favoritePrimes: [2, 3], isNthPrimeButtonDisabled: false ) ) XCTAssert(effects.isEmpty) }

48:44

And now we have tested the entire flow of the user trying to ask for the “nth prime” when the API fails. In particular we are making sure that the alert does not show and that the isNthPrimeButtonDisabled correctly flips back to false so that we can interact with it. Next time: test ergonomics

48:51

We have now written some truly powerful tests. Not only are we testing how the state of the application evolves as the user does various things in the UI, but we are also performing end-to-end testing on effects by asserting that the right effect executes and the right action is returned.

49:13

We do want to mention that the way we have constructed our environments is not 100% ideal right now. It got the job done for this application, but we will run into problems once we want to share a dependency amongst many independent modules, like say our PrimeModal module wanted access to the FileClient . We’d have no choice but to create a new FileClient instance for that module, which would mean the app has two FileClient s floating around. Fortunately, it’s very simple to fix this, and we will be doing that in a future episode really soon.

49:49

Another thing that isn’t so great about our tests is that they’re quite unwieldy. Some of the last tests we wrote are over 60 lines! So if we wrote just 10 tests this file would already be over 600 lines.

50:08

There is a lot of ceremony in our tests right now. We must:

50:11

create expectations run the effects wait for expectations fulfill expectations capture the next action assert what action we got and feed it back into the reducer.

50:26

That’s pretty intense to have to repeat for every effect we test, and as we mentioned it doesn’t even catch the full story of effects since some extra ones could have slipped in.

50:30

Maybe we can focus on the bare essentials: the shape of what we need to do in order to assert expectations against our architecture. It seems to boil down to providing some initial state, providing the reducer we want to test, and then feeding a series of actions and expections along the way, ideally in a declarative fashion with little boilerplate…next time! References Dependency Injection Made Easy Brandon Williams & Stephen Celis • May 21, 2018 We first introduced the Environment concept for controlling dependencies in this episode. Note Today we’re going to control the world! Well, dependencies to the outside world, at least. We’ll define the “dependency injection” problem and show a lightweight solution that can be implemented in your code base with little work and no third party library. https://www.pointfree.co/episodes/ep16-dependency-injection-made-easy Dependency Injection Made Comfortable Brandon Williams & Stephen Celis • Jun 4, 2018 Our second episode on the Environment introduces some patterns around building test data and builds intuitions around identifying the side effects that sneak into our applications. Note Let’s have some fun with the “environment” form of dependency injection we previously explored. We’re going to extract out a few more dependencies, strengthen our mocks, and use our Overture library to make manipulating the environment friendlier. https://www.pointfree.co/episodes/ep18-dependency-injection-made-comfortable How to Control the World Stephen Celis • Sep 24, 2018 Stephen gave a talk on our Environment -based approach to dependency injection at NSSpain 2018. He starts with the basics and slowly builds up to controlling more and more complex dependencies. https://vimeo.com/291588126 Structure and Interpretation of Swift Programs Colin Barrett • Dec 15, 2015 Colin Barrett discussed the problems of dependency injection, the upsides of singletons, and introduced the Environment construct at Functional Swift 2015 . This was the talk that first inspired us to test this construct at Kickstarter and refine it over the years and many other code bases. https://www.youtube.com/watch?v=V-YvI83QdMs Elm: A delightful language for reliable webapps Elm is both a pure functional language and framework for creating web applications in a declarative fashion. It was instrumental in pushing functional programming ideas into the mainstream, and demonstrating how an application could be represented by a simple pure function from state and actions to state. https://elm-lang.org Redux: A predictable state container for JavaScript apps. The idea of modeling an application’s architecture on simple reducer functions was popularized by Redux, a state management library for React, which in turn took a lot of inspiration from Elm . https://redux.js.org Composable Reducers Brandon Williams • Oct 10, 2017 A talk that Brandon gave at the 2017 Functional Swift conference in Berlin. The talk contains a brief account of many of the ideas covered in our series of episodes on “Composable State Management”. https://www.youtube.com/watch?v=QOIigosUNGU Downloads Sample code 0083-testable-state-management-effects 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 .