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.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
In the episode, we asserted against
testSaveButtonTapped
’s save effect by introducing adidSave
boolean variable, which initializesfalse
and toggles totrue
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])) }
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 areceivedValue
expectation and fulfill it in thereceivedValue
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.Create an alternative instance of
FileClient
that saves its data toUserDefaults
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)") } }) }
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 twoFileClients
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 “environment” 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 anEnvironment
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 😂.
- The
References
Dependency Injection Made Easy
Brandon Williams & Stephen Celis • Monday May 21, 2018We 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, 2018Our 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, 2018Stephen 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, 2015Colin 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, 2017A 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”.