A video series exploring functional programming and Swift.
#82 • Monday Nov 25, 2019 • Subscriber-only

Testable State Management: Reducers

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.

This episode builds on concepts introduced previously:

#82 • Monday Nov 25, 2019 • Subscriber-only

Testable State Management: Reducers

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.

This episode builds on concepts introduced previously:


Subscribe to Point‑Free

This episode is for subscribers only. To access it, and all past and future episodes, become a subscriber today!

See subscription optionsorLog in

Sign up for our weekly newsletter to be notified of new episodes, and unlock access to any subscriber-only episode of your choosing!

Sign up for free episode

Introduction

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:

  • The basic units of the architecture should be expressible as simple value types.
  • 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.
  • 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.
  • The architecture should tell you exactly where and how to execute side effects.
  • 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.

SwiftUI gets us really close to solving some of these, but didn’t get us all the way there.

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.

  • State and actions are modeled as value types
  • Mutations are expressed as reducer functions, which are super composable.
  • 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.
  • And most recently we finally showed how side effects can be modeled in this architecture.

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.

So, let’s start by reminding ourselves what we have been building for the past many weeks.

Subscribe to Point-Free

👋 Hey there! Does this episode sound interesting? Well, then you may want to subscribe so that you get access to this episodes and more!


Exercises

  1. Control the save and load effects in the favorite primes module by introducing an “Environment” for dependency injection, as covered in our episode on dependency injection.

  2. Control the save effect inside testSaveButtonTapped to assert that when the reducer is called, it returns the expected effect.

    Making such an assertion involves introducing local, mutable state to the test that is changed in a predictable way if the controlled effect runs. For example, you could introduce a boolean that flips from false to true when the effect runs. Then, after you run the effect returned from the reducer (you can use its sink method), you can assert that it changed as expected.

  3. Further assert that the save effect:

    • Completes. You can use the overload of sink that provides a receivedCompletion handler to hook into this event. Use the expectation and wait methods on the test case to handle this asynchrony.

    • Does not return an action to be fed back into the store. You can introduce an assertion to the receivedValue block that fails if it runs.

  4. Control the load effect inside testLoadButtonTapped to assert that when the reducer is called, it returns the expected effect.

    Assert the load effect completes, as well.

    Further, rather than manually feeding the expected loadedFavoritePrimes action back into the reducer, extract and feed the action returned by the load test effect instead. Again, use the expectation and wait methods to handle this asynchrony.

  5. Continuing the previous exercises, control the nth prime API effect in the counter module such that you can test:

    • That the effect completes
    • That the effect feeds the correct action back into the reducer

References

Chapters
Introduction
00:05
Recap
02:28
Testing the prime modal
06:48
Testing favorite primes
13:54
Testing the counter
19:18
Unhappy paths and integration tests
29:19
Next time: testing effects
34:03