Video #82: Testable State Management: Reducers
Episode: Video #82 Date: Nov 25, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep82-testable-state-management-reducers

Description
It’s time to see how our architecture handles the fifth and final problem we identified as being important to solve when building a moderately complex application: testing! Let’s get our feet wet and write some tests for all of the reducers powering our application.
Video
Cloudflare Stream video ID: 2fd04c6926481afa77bd363d8c4ba889 Local file: video_82_testable-state-management-reducers.mp4 *(download with --video 82)*
References
- Discussions
- Elm: A delightful language for reliable webapps
- Redux: A predictable state container for JavaScript apps.
- Composable Reducers
- 0082-testable-state-management-reducers
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
Many, many weeks ago we built a moderately complex application in SwiftUI from first principles, using only what SwiftUI gave us out of the box ( part 1 , part 2 , part 3 ). We were able to get really far, really quickly, but we noticed a few problems along the way. We distilled what we observed into 5 main problems that we think are crucially important for any application architecture to solve:
— 0:31
The basic units of the architecture should be expressible as simple value types.
— 0:40
Mutations to app state should be done in a single, consistent way, and the units of mutation and observation should be expressed in a way that is composable.
— 0:51
The architecture should be modular, that is you should literally be able to put many units of your application into their own Swift module so that they are fully separated from everything else while still having the ability to be glued together.
— 1:05
The architecture should tell you exactly where and how to execute side effects.
— 1:10
And finally, the architecture should describe how one tests the various components, and ideally a minimal amount of setup work is needed to write these tests.
— 1:15
SwiftUI gets us really close to solving some of these, but didn’t get us all the way there.
— 1:20
This inspired us to embark on exploring an architecture that gave very strong opinions on how each of these problems should be solved, and we’ve fully solved 4 of the 5 problems.
— 1:27
State and actions are modeled as value types
— 1:31
Mutations are expressed as reducer functions, which are super composable.
— 1:38
Observation to state changes are expressed using a store type, which is also composable and allows us to split all of the screens in our app into their own Swift modules that can be run on their own.
— 1:51
And most recently we finally showed how side effects can be modeled in this architecture.
— 1:58
That leaves one last problem to solve, and perhaps the most important: testing. We claim that this architecture is super testable. Pretty much every facet of this architecture can be tested, and it requires very little setup work to write your first test. We will also be able to unlock lots of new ways of testing that are very difficult to achieve without a cohesive and pervasive architecture in your application.
— 2:18
So, let’s start by reminding ourselves what we have been building for the past many weeks. Recap
— 2:28
The app we’ve been building is a counting app with some bells and whistles. You can increment and decrement a current count, ask if it’s a prime number, and if it is, you can add or remove it from a list of favorite primes that persists across screens. You can also ask for the “nth” prime, which fires off an API request to Wolfram Alpha, a computing platform, and when we get a response we show an alert. Further, in the favorite primes screen you can remove any primes that are no longer your favorites, as well as save your favorite primes to disk, and load an older list of favorite primes.
— 3:08
Although this app is mostly a toy example, it still demonstrates a lot of the problems we must solve in any moderately complex application:
— 3:16
It’s got shared state across multiple screens, and when one screen mutates that state it should be instantly reflected in the other screens.
— 3:35
It’s got multiple side effects being used, which adds some complexity to the application. Things like the asynchronous API request to Wolfram Alpha, as well as the synchronous effects that save and load data to disk.
— 3:45
It’s got some subtle logic that it is implementing, such as disabling the nth prime button while the Wolfram API request is inflight.
— 3:58
So this app is pretty complex even though on the surface it only looks like a toy. In fact, it’s getting to the point where it’d be really nice to write some tests. This would help us prove that it currently works in the way we expect, and we’d be able to work on new features while being able to make sure we don’t break current features.
— 4:15
The testing culture in the iOS community isn’t quite as strong as it is in other communities, such as Ruby and JavaScript. Part of this could be due to the fact that languages with type systems tend to require fewer tests since many basic bugs can be caught at compile time. It could also be partly due to the fact that the framework we work in day in and day out is UIKit, which was not really built with testing in mind. There are tools that Apple has given us, like XCUITest, but it is fragile, slow, and aims at testing a very specific part of our applications.
— 4:58
This has naturally led the iOS community to build abstractions and architectures that sit on top of UIKit so that one can more easily write tests. Our community has no shortage of these architectures, and we have even been building our own in the past many episodes of Point-Free, but even with this wealth of knowledge we feel that testing is still not the norm in our community.
— 5:32
The reason, from our experience, has been due to the disconnect of how much work is needed to write a test and how much depth and breadth you get out of the test. Often it takes quite a bit of set up work for a test, everything from creating lots of objects that need to be coordinated, to creating lots of conformances to protocols that were used in the architecture to separate concerns. And the coverage you get out of those tests does not justify the amount of set up required. Many times the test code is so complicated that you end up just verifying that the test code ran correctly without verifying anything from the application itself.
— 5:58
So, if a testing story is going to be successful in an architecture, it’s gotta be straightforward, require very little coordination of additional concepts, and it’s gotta be able to capture very deep, nuanced functionality in our application. That is exactly what we are going to do in our application. We are going to:
— 6:16
Verify that the core logic of our application executes correctly.
— 6:20
Verify that our cross cutting concerns are implemented correctly
— 6:29
Verify that side effects are executed and that their results are fed back into the system correctly
— 6:35
Verify that everything looks correct.
— 6:42
Not only are we going to accomplish all of that, but we are going to be able to do it with a minimal amount of set up work, and we’re even going to have a few surprises of things we can test that seem incredibly difficult to test at first sight. Testing the prime modal
— 6:48
So let’s get started! It’s probably no surprise that the easiest part of our application to test right now is the reducers. That’s because they represent just one single step of moving our application from its current state to its next state, and they are just pure functions. Testing them should be as easy as feeding in a current state and user action and asserting on the resulting state.
— 7:02
Let’s start with one of our simplest screens, the prime modal. This is the screen that is responsible for display the screen that shows whether or not the current count value is prime, and if it is prime it gives you the ability to save or remove it from your list of favorite primes. When we created this module a test target was automatically created, and it already has a test file, which has a bunch of stubbed stuff.
— 7:14
Let’s clean up this file so that we are left with just: import XCTest @testable import PrimeModal class PrimeModalTests: XCTestCase { func testExample() { } }
— 7:18
Let’s first make sure that we can run our test target by selecting the “PrimeModal” target, putting in a dummy assertion: func testExample() { XCTAssertTrue(false) }
— 7:26
And hitting ⌘+U to run the suite. Well, nothing seems to be happening when we hit ⌘+U, and that’s because for whatever reason this test target was not added to the scheme of “PrimeModal”. Let’s do that real quick by hitting “⌘+⇧+,”, selecting the test tab, and adding the “PrimeModalTests” target:
— 7:46
Now when we hit ⌘+U it actually does some building, runs the test, and the test fails as we would expect: XCTAssertTrue failed
— 7:51
That’s great, however I don’t know if you caught what happened, but the build took pretty long to finish, and the test took pretty long to run. It seemed that it built all of the other targets, even though “PrimeModal” doesn’t depend on them, and it launched a simulator to run the tests, even though we are just doing a simple unit test on a function.
— 8:11
This is because right now the test target is configured to run inside a “test host”, in particular it runs inside the full “PrimeTime” application. This is going to greatly slow down our testing feedback cycle, and we can disable it easily enough by setting the host application to “None”.
— 8:28
With all of that done, how do we write our first real test? Well, what does the signature of the prime modal reducer look like? primeModalReducer( state: &<#PrimeModalState#>, action: <#PrimeModalAction#> )
— 8:36
We need some mutable prime modal state to feed to the reducer, as well as an action. For the actions we have a choice between two options: we can either save the current count as a favorite prime, or we can remove it. We want to test both of these code paths.
— 8:52
For adding a favorite prime, let’s start in a state where we have some favorite primes and the current count is on a different prime: var state = (count: 2, favoritePrimes: [3, 5])
— 9:02
Then we can run our reducer on that state to simulate when the user taps on the “save favorite prime” button: primeModalReducer( state: &state, action: .saveFavoritePrimeTapped )
— 9:33
Running this reducer on the state means that the state got mutated in place, and so we’d like to assert what value it changed to. We expect that the number 7 was added to the array of favorite primes, so we might be tempted to do: XCTAssertEqual(state, (2, [3, 5, 2]))
— 9:48
Unfortunately this does not work: Global function ‘ XCTAssertEqual(_:_:_:file:line:) ’ requires that ‘(Int, [Int])’ conform to ‘Equatable’ This error is telling us that we are trying to assert that two things are equal but that their types do not conform to Equatable . And this is because tuples do not conform to Equatable , or any protocol for that matter. We’ve said previously that although it can be nice to write reducers to operate on simple tuples of state, it does come with its drawbacks, and this is one of them.
— 10:03
One thing we could do is assert on each field of the state individually: XCTAssertEqual(state.count, 2) XCTAssertEqual(state.favoritePrimes, [3, 5, 2])
— 10:19
That works, and our tests start passing.
— 10:29
However, we lose exhaustivity of our test. If a field is added to the state, and that field is used in some way in order to execute the logic of saveFavoritePrimeTapped , then we will completely miss out on being able to assert what happened to that field in the reducer. Ideally adding a field to the state would result in a compiler error so that we are forced to consider how that field was changed. So, this isn’t the right way to go.
— 10:44
One thing we can do is explicitly destructure state that we store in the tuple. func testExample() { var state = (count: 2, favoritePrimes: [3, 5]) primeModalReducer( state: &state, action: .saveFavoritePrimeTapped ) let (count, favoritePrimes) = state XCTAssertEqual(count, 2) XCTAssertEqual(favoritePrimes, [3, 5, 2]) }
— 10:53
If prime modal state ever changes, this will fail to compile, forcing us to make changes to our test.
— 11:01
We could have also defined an XCTAssertEqual helper on tuples or upgraded PrimeModalState to be a struct that conforms to Equatable . That would be easy enough, but it can definitely be nice to use tuples in reducers because they are so lightweight and require very little ceremony to work with.
— 11:22
Now if we run our tests: Executed 1 test, with 0 failures (0 unexpected)
— 11:28
Nice! This is what we mean when we say that tests should require very little set up to run. While it’s true that this reducer happens to be very simple right now, the complexity of testing it is only proportional to how many fields state value has. All we have to do is construct a mutable value to plug in, and the assert on the value of the state after the mutation happened. No other set up is required.
— 11:50
However, there is one strange thing happening in this test. We have a warning on the line where we invoke the reducer: Result of call to ‘primeModalReducer(state:action:)’ is unused This is because technically the primeModalReducer function returns an array of effects, and we are not making any assertions whatsoever on what the effects look like. Lucky for us, this reducer doesn’t actually return any effects, so we can simply assert that the array of effects is empty: let effects = primeModalReducer(state: &state, action: .saveFavoritePrimeTapped) let (count, favoritePrimes) = state XCTAssertEqual(state.count, 7) XCTAssertEqual(state.favoritePrimes, [2, 3, 7])) XCTAssert(effects.isEmpty)
— 12:25
This may seem kind of silly to do, but one nice thing about this is that if at a later time we decide to use some effects in this reducer we will get an instant test failure, making it clear to us that we need to update this test to properly accommodate for those new effects. So, even if a reducer doesn’t do have any effects, it can still be a good idea to do this kind of assertion.
— 12:53
This now finishes this one particular test case, but it’s name isn’t quite right, so let’s update it: func testSaveFavoritePrimesTapped() {
— 13:04
Now let’s try testing what happens when one taps the “remove favorite prime” button. We can copy and paste the existing test, and change just a few things:
— 13:11
We’ll rename the test to testRemoveFavoritePrimesTapped
— 13:16
We’ll start in a state where the current count is in the favorite primes array
— 13:22
We’ll assert that the prime was removed from the array when we send the remove action func testRemoveFavoritePrimeTapped() { var state = (count: 3, favoritePrimes: [3, 5]) let effects = primeModalReducer( state: &state, action: .removeFavoritePrimeTapped ) let (count, favoritePrimes) = state XCTAssertEqual(state.count, 3) XCTAssertEqual(state.favoritePrimes, [5])) XCTAssert(effects.isEmpty) }
— 13:35
And boom, just like that we have another passing test. This took very little set up, but it’s testing a real piece of logic that is powering not only this particular screen, but also the favorite primes screen which displays the list of favorite primes. Testing favorite primes
— 13:54
And we have now tested all of the main logic in our prime modal screen. It’s only two tests, but also the reducer is quite simple. Let’s try out a more complicated screen.
— 14:10
The favorite primes screen handles the functionality of displaying our current list of primes, allowing the user to remove a favorite prime, and allowing the user to save their current favorites or load a previously saved list of favorites.
— 14:20
Before we can write any tests for this feature, we gotta do a bit of upfront work like we did for the prime modal:
— 14:25
Add the test target to the scheme
— 14:34
Remove the test host
— 14:37
Clean up the FavoritePrimesTests.swift file
— 14:42
To write our first test we should remind ourselves what it is we are trying to test. Let’s look at what it takes to call the favoritePrimesReducer : favoritePrimesReducer(state: &<#[Int]#>, action: <#FavoritePrimesAction#>)
— 14:50
The state is just a simply array of integers, and the action is one of 4 choice. The simplest of the actions to test is removing a favorite prime: favoritePrimesReducer( state: &<#[Int]#>, action: .deleteFavoritePrimes(<#IndexSet#>) )
— 15:05
To test this we need to provide some state and an index to delete: var state = [2, 3, 5, 7] favoritePrimesReducer( state: &state, action: .deleteFavoritePrimes([2]) )
— 15:27
We expect that this removes the number 5 from the array of favorite primes, so let’s write an assertion: XCTAssertEqual(state, [2, 3, 7])
— 15:41
And this test passes. We do have that warning again because we are not doing anything with the result value of the reducer, but just like last time we want to simply assert that the array of effects is empty: var state = [2, 3, 5, 7] let effects = favoritePrimesReducer( state: &state, action: .deleteFavoritePrimes([2]) ) XCTAssertEqual(state, [2, 3, 7]) XCTAssert(effects.isEmpty)
— 16:04
That was simple enough! Let’s also rename this test case to better reflect what it is for: func testDeleteFavoritePrimes() {
— 16:13
Now let’s try one of the actions. The save button action seems simple enough, let’s see what it takes to test it. We can start with a particular state, invoke the reducer, and assert how the state changed. But, we don’t actually expect the state to change with this action since all it does is save the current list of favorite primes to disk: func testSaveButtonTapped() { var state = [2, 3, 5, 7] let effects = favoritePrimesReducer( state: &state, action: .saveButtonTapped ) XCTAssertEqual(state, [2, 3, 5, 7]) XCTAssert(effects.isEmpty) // ? }
— 16:40
Now, what should we do with this array of effects. This time it isn’t empty, in fact it contains exactly one value: XCTAssertEqual(effects.count, 1)
— 16:57
This assert passes, but it’s a bit silly. We aren’t making any assertions on what happens in the side effect or even what kind of side effect was created, just that some side effect was returned. Unfortunately, that’s the best we can do at this point. We will find a much better way to handle this soon, so let’s leave this as it is right now.
— 17:18
The favorite primes screen also has the functionality for loading a list of primes, which is exposed by a loadButtonTapped action. Let’s copy-paste the test for saving primes and make a few small changes to represent our expectation that state stays the same and we have one effect that’s responsible for loading the primes from disk. func testLoadButtonTapped() { var state = [2, 3, 5, 7] let effects = favoritePrimesReducer( state: &state, action: .loadButtonTapped ) XCTAssertEqual(state, [2, 3, 5, 7]) XCTAssertEqual(effects.count, 1) }
— 17:47
It might be even better to test the entire flow of loading favorite primes, though, which includes a second action, loadedFavoritePrimes action that is sent once the primes are loaded from disk. favoritePrimesReducer( state: &state, action: .loadedFavoritePrimes([2, 31]) )
— 18:08
If we want to hold onto these additional effects, we can make the first variable mutable and reassign the output of calling the reducer a second time. This way we can finally assert against updated state and that this second action produces no additional effects. 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)
— 18:31
Finally we could maybe update the test name to better describe the fact that we’re testing the entire flow. func testLoadFavoritePrimesFlow() {
— 18:42
This tests the core reducer logic, in that we are verifying that the state does not change immediately when we tell the reducer to load the favorite primes, but it does change once we get a response from the effect that loads the primes from disk.
— 18:48
But again, there is some silly stuff happening in this test. First, it got a little messy that we had to keep around a mutable effects value so that we could overwrite it in our second invocation of the reducer. Second, it’s still weird that we are only asserting that some effect was returned but we aren’t asserting anything on the contents of that effect. And finally, it’s strange that we have to explicitly feed the result of the effect back into the reducer. It would be best if this could be done automatically so that we don’t even have to remember to do this in our test, and even better if we ever refactor the reducer to emit different kinds of effects we won’t have to remember to update tests.
— 19:12
However, we’re still not quite ready to attack those problems. We will get there soon, but let’s continue understanding how reducers can be tested before we start attacking that much harder problem. Testing the counter
— 19:18
The only other screen we haven’t written tests for is the counter screen, which allows the user to increment and decrement the current count, as well as ask for the “nth prime” based on what the current count is.
— 19:34
Before we can test any of that we gotta prep the test target once again:
— 19:40
Add the test target to the scheme
— 19:44
Remove the test host
— 19:47
Clean up the CounterTests.swift file
— 19:54
To figure out what we should test first, let’s remember that the couterViewReducer takes CounterViewAction and CounterViewState as input.
— 20:17
CounterViewState is a struct, so we can initialize one to feed into the reducer. var state = CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false )
— 20:52
The CounterViewAction has two cases: one for CounterAction s and another for PrimeModalAction s. The latter we’ve already tested, so we mostly want to concentrate on CounterAction s.
— 21:15
The CounterAction contains a number of cases. Let’s start with some of the simpler ones. For example, to test that when the user taps on the increment button the count goes up by one we can simply do: counterViewReducer(&state, .counter(.incrTapped)) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 3, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false ) )
— 21:43
However, we immediately get the following compiler error: Global function ‘ XCTAssertEqual(_:_:_:file:line:) ’ requires that ‘CounterViewState’ conform to ‘Equatable’ Turns out our CounterViewState doesn’t conform to Equatable yet. That’s easy enough to fix: public struct CounterViewState: Equatable {
— 21:55
But that causes another compiler error because PrimeAlert doesn’t conform to Equatable , but we can also fix that: public struct PrimeAlert: Equatable, Identifiable {
— 22:05
And now we’re in building order, and tests pass, which means we are asserting that when the increment button is tapped, the count goes up by one but no other state changes.
— 22:18
Of course, we should also capture the effects and assert that we expect none. let effects = counterViewReducer(&state, .counter(.incrTapped)) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 3, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false ) ) XCTAssert(effects.isEmpty)
— 22:38
And finally, let’s update the test’s name to be more descriptive. func testIncrButtonTapped() {
— 22:44
We can even copy and paste this test and make a few small changes to get an equivalent test that verifies the decrement logic works correctly: func testDecrButtonTapped() { var state = CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false ) let effects = counterViewReducer(&state, .counter(.decrTapped)) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 1, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false ) ) XCTAssert(effects.isEmpty) }
— 22:59
The only other logic to test in this reducer is the flow of tapping on the “what is the nth prime” button and getting a response from the Wolfram API. We’ve already seen that testing reducers with effects isn’t ideal, but let’s just see how far we can get.
— 23:24
We can start by instantiating a state that we want to begin with: func testNthPrimeButtonFlow() { var state = CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false )
— 23:37
The first user action that happens in this flow is that the user taps on the “nth prime” button: var effects = counterViewReducer( &state, .counter(.nthPrimeButtonTapped) )
— 23:54
When this happens we expect the “nth prime” button to become disabled, because the Wolfram API request is now inflight, and a single effect will be emitted (but again we don’t know anything about this effect right now): XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: true ) ) XCTAssertEqual(effects.count, 1)
— 24:30
Although we can’t assert anything on the effect returned, we do know that at some point the API request will finish and its result will be fed back into the store. We can just do this manually for now by invoking the reducer again with the nthPrimeResponse action: effects = counterViewReducer( &state, .counter(.nthPrimeResponse(3)) ) XCTAssertEqual( state, CounterViewState( alertNthPrime: PrimeAlert(prime: 3), count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false ) ) XCTAssert(effects.isEmpty) Here the state changes in two ways: first the prime alert becomes non-nil and holds the number 3 (because the 2nd prime is 3), and the “nth prime” button’s disable state flips to false now that the API request has finished.
— 26:18
There is one further step of this flow: the user can dismiss the alert that is currently showing in the UI. When this happens we expect that the alert state switches back to nil : effects = counterViewReducer(&state, .counter(.alertDismissButtonTapped)) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false ) ) XCTAssert(effects.isEmpty)
— 27:08
That completes the test of this flow, and if we run tests everything still passes.
— 27:16
It’s worth mentioning how cool this is, even though we aren’t testing the effects at all. What we have here is a script of user actions:
— 27:27
The user taps on a button The API response comes back The user dismisses the alert
— 27:50
And we get to assert on the state of the application after each user action, including making sure that a button was disabled and then became enabled, and that an alert showed and then was dismissed. This is already a pretty broad test to be writing, with very little upfront work.
— 28:00
Also, it is fair to say that this test is actually testing what will happen in the UI because the implementation of the view property in the counter view does the most basic thing with this state: .disabled(self.store.value.isNthPrimeButtonDisabled) … .alert(item: .constant(self.store.value.alertNthPrime))
— 28:09
There’s no logic in the view regarding these values. As long as the reducer produces the right state, everything should work properly, and we now have a test covering that logic.
— 28:52
Further, this style of architecture and testing plays very nicely with “test-driven development” if that’s something you value. In this style of testing you would set up your state struct, action enum, and signature of a reducer that doesn’t have any logic, it could just return an empty array of effects. You would then write out this entire test suite based on how you know the feature should work, and then go in and implement the reducer until the test suite passes. Unhappy paths and integration tests
— 29:19
So now the counter view is pretty well tested, and we could even stop here and we’ve got a pretty nice test suite, except for handling effects while we’ll be doing shortly. However, we could take this test suite to the next level if we wanted to. First, we’re only testing the happy path of tapping the nth prime button. We should also test the unhappy path in case of failure.
— 29:47
The counter view also has all of the functionality of the prime modal, which we have already covered in some isolated tests in the PrimeModal module. But, perhaps we want to exercise some prime modal functionality in the context of the counter too. Maybe we want to make sure that the prime modal reducer isn’t accidentally messing up any of the counter view state. We can do this very easily by writing an integration-style test.
— 30:13
The current flow we’re testing is the happy flow, so let’s reflect that in the test name. func testNthPrimeButtonHappyFlow() {
— 30:19
We also want to capture the unhappy flow, so we can copy and paste the happy flow and make a few changes. Notably, that we get a nil response back from the API, and never display the alert, so the dismissal action goes away. func testNthPrimeButtonUnhappyFlow() { var state = CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false ) var effects = counterViewReducer( &state, .counter(.nthPrimeButtonTapped) ) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: true ) ) XCTAssertEqual(effects.count, 1) effects = counterViewReducer( &state, .counter(.nthPrimeResponse(nil) ) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false ) ) XCTAssert(effects.isEmpty) }
— 31:02
And when we run the test, it passes! With very little work we were able to test both happy and unhappy paths for this flow.
— 31:09
Another test that we can write, which is really cool, is an integration-style test that ensures the counter and prime modal have been properly integrated together. func testPrimeModal() { var state = CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false ) var effects = counterViewReducer( &state, .primeModal(.saveFavoritePrimeTapped) ) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5, 2], isNthPrimeButtonDisabled: false ) ) XCTAssert(effects.isEmpty) effects = counterViewReducer( &state, .primeModal(.removeFavoritePrimeTapped) ) XCTAssertEqual( state, CounterViewState( alertNthPrime: nil, count: 2, favoritePrimes: [3, 5], isNthPrimeButtonDisabled: false ) ) XCTAssert(effects.isEmpty) }
— 33:02
Here we are verifying that the prime modal is correctly functioning when embedded in the counter view, and that its functionality doesn’t break anything in the counter view. We could even go through an entire user script that goes through the flow of adding a favorite prime, asking for the nth prime, and then removing a favorite prime, while verifying at each step of the way that the state is changed in the way we expect.
— 33:24
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. Next time: testing effects
— 34:03
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.
— 34:16
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.
— 34:39
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.
— 35:07
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.
— 35:24
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?
— 35:30
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…next time! References 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 0082-testable-state-management-reducers 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 .