Testable State Management: Effects

Episode #83 • Dec 2, 2019 • Subscriber-Only

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.

Effects
Introduction
00:05
Testing effects
00:44
Recap: the environment
02:36
Controlling the favorite primes save effect
07:44
Controlling the favorite primes load effect
17:06
Testing the favorite primes save effect
24:19
Testing the favorite primes load effect
31:31
Controlling the counter effect
37:51
Testing the counter effects
42:25
Next time: test ergonomics
48:51

Unlock This Episode

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

Introduction

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.

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. In the episode, we asserted against testSaveButtonTapped’s save effect by introducing a didSave boolean variable, which initializes false and toggles to true in the environment-controlled effect.

    While this tests that the effect executes, it does not test that the proper data was fed into the effect in the first place. In fact, it ignores this argument entirely!

    Strengthen this test so that it asserts that the correct data was sent to the effect. This will give us test coverage on the JSON encoding logic that is currently in the reducer.

    Solution
    func testSaveButtonTapped() {
      Current = .mock
      var encodedPrimes: Data?
      Current.fileClient.save = { _, data in
        .fireAndForget {
          encodedPrimes = data
        }
      }
    
      var state = [2, 3, 5, 7]
      let effects = favoritePrimesReducer(state: &state, action: .saveButtonTapped)
    
      XCTAssertEqual(state, [2, 3, 5, 7])
      XCTAssertEqual(effects.count, 1)
    
      effects[0].sink { _ in XCTFail() }
    
      XCTAssertEqual(encodedPrimes, try JSONEncoder().encode([2, 3, 5, 7]))
    }
    
  2. Our testLoadButtonTapped passed even when we fed multiple actions back from a single effect.

    Strengthen this test to ensure that only a single action is ever fed back into the reducer.

    Solution

    There are many ways of asserting that only a single value is received, including introducing a mutable tally of how many times receivedValue was called. A simple solution, though, is to introduce a receivedValue expectation and fulfill it in the receivedValue block:

    func testLoadFavoritePrimesFlow() {
      Current.fileClient.load = { _ in .sync { try! JSONEncoder().encode([2, 31]) } }
    
      var state = [2, 3, 5, 7]
      var effects = favoritePrimesReducer(state: &state, action: .loadButtonTapped)
    
      XCTAssertEqual(state, [2, 3, 5, 7])
      XCTAssertEqual(effects.count, 1)
    
      var nextAction: FavoritePrimesAction!
      let receivedCompletion = self.expectation(description: "receivedCompletion")
      let receivedValue = self.expectation(description: "receivedValue")
      effects[0].sink(
        receiveCompletion: { _ in
          receivedCompletion.fulfill()
      },
        receiveValue: { action in
          receivedValue.fulfill()
          XCTAssertEqual(action, .loadedFavoritePrimes([2, 31]))
          nextAction = action
      })
      self.wait(for: [receivedValue, receivedCompletion], timeout: 0)
    
      effects = favoritePrimesReducer(state: &state, action: nextAction)
    
      XCTAssertEqual(state, [2, 31])
      XCTAssert(effects.isEmpty)
    }
    

    If receivedValue is fulfilled more than once, it will cause the test to fail.

  3. Create an alternative instance of FileClient that saves its data to UserDefaults instead of the file system.

    Solution
    extension FileClient {
      static let userDefaults = FileClient(
        load: { fileName -> Effect<Data?> in
          .sync {
            UserDefaults.standard.data(forKey: "FileClient:\(fileName)")
          }
      },
        save: { fileName, data in
          return .fireAndForget {
            UserDefaults.standard.set(data, forKey: "FileClient:\(fileName)")
          }
      })
    }
    
  4. One problem with using an environment struct in each module is that it does not play nicely with sharing dependencies across module boundaries. For example, suppose another module needed a FileClient. You would have no choice but to have two FileClients alive in your application, one for each module, which means you would need to remember to mock both when you want to write tests.

    One way to fix this is to bake the notion of “ennvironment” directly into the reducer signature. Try this out by updating Reducer to be the following shape:

    typealias Reducer<Value, Action, Environment> =
      (inout Value, Action, Environment) -> [Effect<Action>]
    

    This gives you access to the environment in a reducer, which means you can use it to construct effects to be returned. However, this will cause a lot of compiler errors, and to get everything in working order here are some things to work through:

    • The pullback operation needs to be updated to work with this new reducer signature. Is it necessary to pullback along key paths like was done for state and actions, or will a plain function suffice?
    • The Store class needs to be updated since it holds onto a reducer and that has signature has changed. One way to fix this would be to introduce an Environment generic. However, the environment isn’t actually use in its public API, which means maybe we can hide this detail from the type using type erasure.
    • Update modules that use an environment to include their environment directly in the reducer and remove the Current globals.
    Solution

    This will be a future Point-Free episode 😂.

References

Dependency Injection Made Easy

Brandon Williams & Stephen Celis • Monday May 21, 2018

We first introduced the Environment concept for controlling dependencies in this episode.

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.

Dependency Injection Made Comfortable

Brandon Williams & Stephen Celis • Monday 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.

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.

How to Control the World

Stephen Celis • Monday 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.

Structure and Interpretation of Swift Programs

Colin Barrett • Tuesday 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.

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”.