The Startup
Published in

The Startup

Enable synchronous testability of your reactive Combine code using injectable Schedulers

co-authored with Taylor Lindsay

Combine is Apple’s new framework for writing reactive code. It’s pretty similar to its 3rd party analogue RxSwift. This framework allows you to write asynchronous event handling in a functional manner using different processing operators.

This framework is a really powerful tool that provides an efficient way for writing asynchronous code, performing delayed operations, synchronizing your data, and working with different queues inside your app. With great power comes great responsibility; that’s why it’s really tricky sometimes to support and maintain reactive code, especially if we are talking about testing approaches. In this article, we will talk about how we can test reactive code synchronously.

Problem description

While working in a multithreaded environment we mostly use DispatchQueuewith Grand Central Dispatch API to control the thread/queue we are working on. To make your app as efficient as possible you should always be aware how and where to process your data. Some tasks are not so important, and don’t need to be executed immediately so you can perform them in background. Some tasks are crucial for best user experience so they should be performed in a more prioritised way. There are also tasks that must be executed on specific queues to make your app properly work (such as UI related tasks).

Combine framework provides a convenient API for such things and introduces Scheduler protocol. By using different schedulers engineers can specify how and when the code will be executed. This allows us to use different operators such as debounce, throttle, delay etc. The moment you use Scheduler your code becomes asynchronous which makes it hard to test as a test function completes before your async code is executed. So let’s take a look at few examples of how we can test reactive code.

XCTestExpectation

Using XCTestExpectation is the most intuitive approach, which provides the functionality to wait for asynchronous code completion using expectations. Let’s create a test SwiftUI application with reactive Combine streams and see how tests will look.

Let’s create a dummy TextService which provides a text value 2 seconds after we subscribe to it:

Now let’s create a ViewModel with TextService injected and some logic connecting this ViewModel to a View :

As you might see we fetch a text from TextService and propagate it to @Published var text: String? on the main queue.

And as a final step let’s connect it to a SwiftUI View:

After running the code we can observe the text is displayed after 2 seconds.

Now let’s test ViewModel with XCTestExpectation:

As you can see the instance of XCTestExpectation is created and it requires to be fulfilled in specified timeout period. This approach forces us to control the expectation manually knowing how and when to fulfill it. Also we need to wait for the expectation so it makes our test cases run slower.

Injectable Scheduler

The other solution is to make our scheduler injectable so we can mock it and have a full control over the code execution:

Unfortunately this is impossible as Scheduler is a protocol with two associated types inside (SchedulerTimeType and SchedulerOptions) so we can use it only as a generic constraint.

To make it possible to inject Scheduler, the type can be erased. So we can wrap base scheduler into AnyScheduler. Now we can easily inject any type of schedulers using .eraseToAnyScheduler() which follows Apple’s naming conventions.

With AnyScheduler our ViewModel will look a bit different:

This approach allows us to inject any type of scheduler, which will execute code synchronously. For our needs we’ve implemented TestScheduler. This scheduler can store all actions and execute them immediately ignoring the delay. Let’s use this scheduler in tests:

As you can see now it’s possible to write unit tests synchronously without using expectations and wait functions. The above approach speeds up the test execution time drastically on a larger scale, which saves developer and CI time.

Summary

By using injectable Scheduler solution, developers can write synchronous tests for asynchronous Combine code. This approach makes the process of writing tests faster and more unified. A nice bonus of using this approach is reduced time for test execution, which saves developers’ time.

Thanks a lot for reading! If this article was useful please leave a comment and share it with someone who might also find it interesting and helpful :)

The full example project can be found here.

SchedulerKit source code:

The library contains source code for type-erased schedulers as well as TestScheduler implementation. Be free to contribute!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store