Testable State Management: Reducers

Episode #82 • Nov 25, 2019 • Subscriber-Only

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.

Reducers
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

Unlock This Episode

Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.

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.

This episode is for subscribers only.

Subscribe to Point-Free

Access this episode, plus all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

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

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.

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.

Composable Reducers

Brandon Williams • Tuesday 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”.