Better Test Dependencies: Immediacy

Episode #140 • Mar 29, 2021 • Subscriber-Only

A major source of complexity in our applications is asynchrony. It is a side effect that is easy to overlook and can make testing more difficult and less reliable. We will explore the problem and come to a solution using Combine schedulers.

Immediacy
Introduction
00:05
Designing dependencies: recap and problem
02:49
Controlling our dispatches to main
10:17
Immediate schedulers
22:28
Where immediacy fails us
33:45
Next time: the point
38:21

Unlock This Episode

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

Introduction

So this is pretty amazing. We are now getting a ton of insight into our code base by embracing exhaustive dependencies, and in particular invoking XCTFail immediately for any dependencies that we do not expect to be called. This allows us to be instantly notified when one of our features starts accessing a dependency we don’t expect, and on the flip side allows us to introduce new dependencies to our feature and be instantly notified of which tests need to be updated.

But there’s still more to discover. Failing dependencies greatly improved the developer experience when writing tests, but there is still room for more improvement. When we write tests that deal with time, such as delaying or debouncing, we like to use a test scheduler because it allows us to deterministically control the flow of time. We even have a todo test that specifically asserts that when you complete a todo, wait half a second, then complete another todo, and then wait a full second, that the todos were not sorted until the full one and a half seconds passed. It could actually capture that intermediate moment where the second todo’s completion cancelled the sorting effect. And that’s incredibly powerful.

However, sometimes we deal with schedulers that do not involve the passage of time. They are just used to execute on specific queues, such as when you use the .subscribe(on:) or .receive(on:) operator. If we use the test scheduler for these situations we have to litter our tests with scheduler.advance() calls in order to push them a tick forward and execute their work. Sometimes you do really want that, like if you want to test some synchronous effects that run before an asynchronous effect. However, most of the times it’s an unnecessary annoyance, and we can definitely improve it.

Even better, by addressing this test annoyance we’ll actually unlock something really cool for SwiftUI previews. We’ll show how we can exercise more of our feature’s logic using static previews when typically you would have to resort to running the live preview.

Let’s start by demonstrating the problem that test schedulers can cause. We are going to resurrect the project we built for our “Designing Dependencies” series of episodes. In those episodes we built a moderately complex application that made use of an API client, a location manager, and a network monitor in order to implement a simple weather app. Let’s recap:

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. Define immediate schedulers for OperationQueue and RunLoop.

    Solution

    This can be done exactly the same as we did for failing schedulers last week!

    extension Scheduler
    where
      SchedulerTimeType == OperationQueue.SchedulerTimeType,
      SchedulerOptions == OperationQueue.SchedulerOptions
    {
      public static var immediate: AnySchedulerOf<Self> {
        .immediate(now: .init(Date()))
      }
    }
    
    extension Scheduler
    where
      SchedulerTimeType == RunLoop.SchedulerTimeType,
      SchedulerOptions == RunLoop.SchedulerOptions
    {
      public static var immediate: AnySchedulerOf<Self> {
        .immediate(now: .init(Date()))
      }
    }
    
  2. Implement cancellation in the designing dependencies application. This could be as simple as adding a button to the view. Feel free to get creative!

  3. With cancellation implemented in the UI, it’d be nice to be able to see it working in the preview. To do so, introduce a badWifi weather client, which slows each of its endpoints down by several seconds.

  4. What happens to the UI if we cancel things before the first API request to fetch locations succeeds? What happens to the UI if we cancel things after it succeeds? How might we improve the user experience?

References

Collection: Schedulers

Brandon Williams & Stephen Celis • Thursday Jun 4, 2020

There’s a lot of great material in the community covering almost every aspect of the Combine framework, but sadly Combine’s Scheduler protocol hasn’t gotten much attention. It’s a pretty mysterious protocol, and Apple does not provide much documentation about it, but it is incredibly powerful and can allow one to test how time flows through complex publishers.

Designing Dependencies

Brandon Williams & Stephen Celis • Monday Jul 27, 2020

We develop the idea of dependencies from the ground up in this collection of episodes:

Let’s take a moment to properly define what a dependency is and understand why they add so much complexity to our code. We will begin building a moderately complex application with three dependencies, and see how it complicates development, and what we can do about it.

Composable Architecture: Dependency Management

Brandon Williams & Stephen Celis • Monday Feb 17, 2020

We made dependencies a first class concern of the Composable Architecture by baking the notion of dependencies directly into the definition of its atomic unit: the reducer.

Composable Architecture

Brandon Williams & Stephen Celis • Monday May 4, 2020

The Composable Architecture is a library for building applications in a consistent and understandable way, with composition, testing and ergonomics in mind.