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.
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.
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
TextService injected and some logic connecting this
ViewModel to a
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
After running the code we can observe the text is displayed after 2 seconds.
Now let’s test
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.
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 (
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.
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.
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:
Combine injectable Scheduler library . Contribute to scheduler-kit/SchedulerKit development by creating an account on…
The library contains source code for type-erased schedulers as well as
TestScheduler implementation. Be free to contribute!